summaryrefslogtreecommitdiff
path: root/packages/client/src
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-12-18 01:59:59 +0900
committerGitHub <noreply@github.com>2022-12-18 01:59:59 +0900
commit4ecc42744c3c8b68e38f58bfe03919bf437f137a (patch)
tree094230a6992c05cf39136913d02400fae27931d7 /packages/client/src
parentfix(server): GitHubログインしようとするとreply.setCookie is not a ... (diff)
downloadsharkey-4ecc42744c3c8b68e38f58bfe03919bf437f137a.tar.gz
sharkey-4ecc42744c3c8b68e38f58bfe03919bf437f137a.tar.bz2
sharkey-4ecc42744c3c8b68e38f58bfe03919bf437f137a.zip
enhance: Implement the toggle to (or not to) close push notifications when notifications or messages are read (#9219)
* create file * wip * fix * wip * tabun dekita * :v: * implement subscribe push notification button to tutorial * check-exists→show-registration * add column sendReadMessage * fix migration file * sw api * change PushNotificationService * wip * :v: * fix tutorial footer flex
Diffstat (limited to 'packages/client/src')
-rw-r--r--packages/client/src/components/MkPushNotificationAllowButton.vue167
-rw-r--r--packages/client/src/pages/settings/notifications.vue30
-rw-r--r--packages/client/src/pages/timeline.tutorial.vue116
-rw-r--r--packages/client/src/scripts/initialize-sw.ts55
4 files changed, 266 insertions, 102 deletions
diff --git a/packages/client/src/components/MkPushNotificationAllowButton.vue b/packages/client/src/components/MkPushNotificationAllowButton.vue
new file mode 100644
index 0000000000..a762914e64
--- /dev/null
+++ b/packages/client/src/components/MkPushNotificationAllowButton.vue
@@ -0,0 +1,167 @@
+<template>
+<MkButton
+ v-if="supported && !pushRegistrationInServer"
+ type="button"
+ primary
+ :gradate="gradate"
+ :rounded="rounded"
+ :inline="inline"
+ :autofocus="autofocus"
+ :wait="wait"
+ :full="full"
+ @click="subscribe"
+>
+ {{ i18n.ts.subscribePushNotification }}
+</MkButton>
+<MkButton
+ v-else-if="!showOnlyToRegister && ($i ? pushRegistrationInServer : pushSubscription)"
+ type="button"
+ :primary="false"
+ :gradate="gradate"
+ :rounded="rounded"
+ :inline="inline"
+ :autofocus="autofocus"
+ :wait="wait"
+ :full="full"
+ @click="unsubscribe"
+>
+ {{ i18n.ts.unsubscribePushNotification }}
+</MkButton>
+<MkButton v-else-if="$i && pushRegistrationInServer" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
+ {{ i18n.ts.pushNotificationAlreadySubscribed }}
+</MkButton>
+<MkButton v-else-if="!supported" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
+ {{ i18n.ts.pushNotificationNotSupported }}
+</MkButton>
+</template>
+
+<script setup lang="ts">
+import { $i, getAccounts } from '@/account';
+import MkButton from '@/components/MkButton.vue';
+import { instance } from '@/instance';
+import { api, apiWithDialog, promiseDialog } from '@/os';
+import { i18n } from '@/i18n';
+
+defineProps<{
+ primary?: boolean;
+ gradate?: boolean;
+ rounded?: boolean;
+ inline?: boolean;
+ link?: boolean;
+ to?: string;
+ autofocus?: boolean;
+ wait?: boolean;
+ danger?: boolean;
+ full?: boolean;
+ showOnlyToRegister?: boolean;
+}>();
+
+// ServiceWorker registration
+let registration = $ref<ServiceWorkerRegistration | undefined>();
+// If this browser supports push notification
+let supported = $ref(false);
+// If this browser has already subscribed to push notification
+let pushSubscription = $ref<PushSubscription | null>(null);
+let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
+
+function subscribe() {
+ if (!registration || !supported || !instance.swPublickey) return;
+
+ // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
+ return promiseDialog(registration.pushManager.subscribe({
+ userVisibleOnly: true,
+ applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
+ })
+ .then(async subscription => {
+ pushSubscription = subscription;
+
+ // Register
+ pushRegistrationInServer = await api('sw/register', {
+ endpoint: subscription.endpoint,
+ auth: encode(subscription.getKey('auth')),
+ publickey: encode(subscription.getKey('p256dh'))
+ });
+ }, async err => { // When subscribe failed
+ // 通知が許可されていなかったとき
+ if (err?.name === 'NotAllowedError') {
+ console.info('User denied the notification permission request.');
+ return;
+ }
+
+ // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
+ // 既に存在していることが原因でエラーになった可能性があるので、
+ // そのサブスクリプションを解除しておく
+ // (これは実行されなさそうだけど、おまじない的に古い実装から残してある)
+ await unsubscribe();
+ }), null, null);
+}
+
+async function unsubscribe() {
+ if (!pushSubscription) return;
+
+ const endpoint = pushSubscription.endpoint;
+ const accounts = await getAccounts();
+
+ pushRegistrationInServer = undefined;
+
+ if ($i && accounts.length >= 2) {
+ apiWithDialog('sw/unregister', {
+ i: $i.token,
+ endpoint,
+ });
+ } else {
+ pushSubscription.unsubscribe();
+ apiWithDialog('sw/unregister', {
+ endpoint,
+ });
+ pushSubscription = null;
+ }
+}
+
+function encode(buffer: ArrayBuffer | null) {
+ return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
+}
+
+/**
+ * Convert the URL safe base64 string to a Uint8Array
+ * @param base64String base64 string
+ */
+ function urlBase64ToUint8Array(base64String: string): Uint8Array {
+ const padding = '='.repeat((4 - base64String.length % 4) % 4);
+ const base64 = (base64String + padding)
+ .replace(/-/g, '+')
+ .replace(/_/g, '/');
+
+ const rawData = window.atob(base64);
+ const outputArray = new Uint8Array(rawData.length);
+
+ for (let i = 0; i < rawData.length; ++i) {
+ outputArray[i] = rawData.charCodeAt(i);
+ }
+ return outputArray;
+}
+
+navigator.serviceWorker.ready.then(async swr => {
+ registration = swr;
+
+ pushSubscription = await registration.pushManager.getSubscription();
+
+ if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
+ supported = true;
+
+ if (pushSubscription) {
+ const res = await api('sw/show-registration', {
+ endpoint: pushSubscription.endpoint,
+ });
+
+ if (res) {
+ pushRegistrationInServer = res;
+ }
+ }
+ }
+});
+
+defineExpose({
+ pushRegistrationInServer: $$(pushRegistrationInServer),
+});
+</script>
diff --git a/packages/client/src/pages/settings/notifications.vue b/packages/client/src/pages/settings/notifications.vue
index 5703e0c6b6..77ec567da4 100644
--- a/packages/client/src/pages/settings/notifications.vue
+++ b/packages/client/src/pages/settings/notifications.vue
@@ -6,6 +6,18 @@
<FormLink class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormLink>
<FormLink class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormLink>
</FormSection>
+ <FormSection>
+ <template #label>{{ i18n.ts.pushNotification }}</template>
+ <MkPushNotificationAllowButton ref="allowButton" />
+ <FormSwitch class="_formBlock" :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:modelValue="onChangeSendReadMessage">
+ <template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
+ <template #caption>
+ <I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
+ <template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
+ </I18n>
+ </template>
+ </FormSwitch>
+ </FormSection>
</div>
</template>
@@ -15,10 +27,16 @@ import { notificationTypes } from 'misskey-js';
import FormButton from '@/components/MkButton.vue';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
+import FormSwitch from '@/components/form/switch.vue';
import * as os from '@/os';
import { $i } from '@/account';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
+import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
+
+let allowButton = $ref<InstanceType<typeof MkPushNotificationAllowButton>>();
+let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
+let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
async function readAllUnreadNotes() {
await os.api('i/read-all-unread-notes');
@@ -49,6 +67,18 @@ function configure() {
}, 'closed');
}
+function onChangeSendReadMessage(v: boolean) {
+ if (!pushRegistrationInServer) return;
+
+ os.apiWithDialog('sw/update-registration', {
+ endpoint: pushRegistrationInServer.endpoint,
+ sendReadMessage: v,
+ }).then(res => {
+ if (!allowButton) return;
+ allowButton.pushRegistrationInServer = res;
+ });
+}
+
const headerActions = $computed(() => []);
const headerTabs = $computed(() => []);
diff --git a/packages/client/src/pages/timeline.tutorial.vue b/packages/client/src/pages/timeline.tutorial.vue
index 7f08ccc2a1..9683cc22a5 100644
--- a/packages/client/src/pages/timeline.tutorial.vue
+++ b/packages/client/src/pages/timeline.tutorial.vue
@@ -1,6 +1,17 @@
<template>
-<div class="_card tbkwesmv">
- <div class="_title"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+<div class="_card">
+ <div :class="$style.title" class="_title">
+ <div :class="$style.titleText"><i class="fas fa-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+ <div :class="$style.step">
+ <button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
+ <i class="fas fa-chevron-left"></i>
+ </button>
+ <span :class="$style.stepNumber">{{ tutorial + 1 }} / {{ tutorialsNumber }}</span>
+ <button class="_button" :class="$style.stepArrow" :disabled="tutorial === tutorialsNumber - 1" @click="tutorial++">
+ <i class="fas fa-chevron-right"></i>
+ </button>
+ </div>
+ </div>
<div v-if="tutorial === 0" class="_content">
<div>{{ i18n.ts._tutorial.step1_1 }}</div>
<div>{{ i18n.ts._tutorial.step1_2 }}</div>
@@ -15,7 +26,7 @@
<div>{{ i18n.ts._tutorial.step3_1 }}</div>
<div>{{ i18n.ts._tutorial.step3_2 }}</div>
<div>{{ i18n.ts._tutorial.step3_3 }}</div>
- <small>{{ i18n.ts._tutorial.step3_4 }}</small>
+ <small :class="$style.small">{{ i18n.ts._tutorial.step3_4 }}</small>
</div>
<div v-else-if="tutorial === 3" class="_content">
<div>{{ i18n.ts._tutorial.step4_1 }}</div>
@@ -32,7 +43,7 @@
</template>
</I18n>
<div>{{ i18n.ts._tutorial.step5_3 }}</div>
- <small>{{ i18n.ts._tutorial.step5_4 }}</small>
+ <small :class="$style.small">{{ i18n.ts._tutorial.step5_4 }}</small>
</div>
<div v-else-if="tutorial === 5" class="_content">
<div>{{ i18n.ts._tutorial.step6_1 }}</div>
@@ -48,19 +59,20 @@
</I18n>
<div>{{ i18n.ts._tutorial.step7_3 }}</div>
</div>
+ <div v-else-if="tutorial === 7" class="_content">
+ <div>{{ i18n.ts._tutorial.step8_1 }}</div>
+ <div>{{ i18n.ts._tutorial.step8_2 }}</div>
+ <small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
+ </div>
- <div class="_footer navigation">
- <div class="step">
- <button class="arrow _button" :disabled="tutorial === 0" @click="tutorial--">
- <i class="fas fa-chevron-left"></i>
- </button>
- <span>{{ tutorial + 1 }} / 7</span>
- <button class="arrow _button" :disabled="tutorial === 6" @click="tutorial++">
- <i class="fas fa-chevron-right"></i>
- </button>
- </div>
- <MkButton v-if="tutorial === 6" class="ok" primary @click="tutorial = -1"><i class="fas fa-check"></i> {{ i18n.ts.gotIt }}</MkButton>
- <MkButton v-else class="ok" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
+ <div class="_footer" :class="$style.footer">
+ <template v-if="tutorial === tutorialsNumber - 1">
+ <MkPushNotificationAllowButton :class="$style.footerItem" primary show-only-to-register @click="tutorial = -1" />
+ <MkButton :class="$style.footerItem" :primary="false" @click="tutorial = -1">{{ i18n.ts.noThankYou }}</MkButton>
+ </template>
+ <template v-else>
+ <MkButton :class="$style.footerItem" primary @click="tutorial++"><i class="fas fa-check"></i> {{ i18n.ts.next }}</MkButton>
+ </template>
</div>
</div>
</template>
@@ -68,53 +80,63 @@
<script lang="ts" setup>
import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
+import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
+const tutorialsNumber = 8;
+
const tutorial = computed({
get() { return defaultStore.reactiveState.tutorial.value || 0; },
set(value) { defaultStore.set('tutorial', value); },
});
</script>
-<style lang="scss" scoped>
-.tbkwesmv {
- > ._content {
- > small {
- opacity: 0.7;
- }
- }
+<style lang="scss" module>
+.small {
+ opacity: 0.7;
+}
- > .navigation {
- display: flex;
- flex-direction: row;
- align-items: baseline;
+.title {
+ display: flex;
+ flex-wrap: wrap;
- > .step {
- > .arrow {
- padding: 4px;
+ &Text {
+ margin: 4px 0;
+ padding-right: 4px;
+ }
+}
- &:disabled {
- opacity: 0.5;
- }
+.step {
+ margin-left: auto;
- &:first-child {
- padding-right: 8px;
- }
+ &Arrow {
+ padding: 4px;
+ &:disabled {
+ opacity: 0.5;
+ }
+ &:first-child {
+ padding-right: 8px;
+ }
+ &:last-child {
+ padding-left: 8px;
+ }
+ }
- &:last-child {
- padding-left: 8px;
- }
- }
+ &Number {
+ font-weight: normal;
+ margin: 4px;
+ }
+}
- > span {
- margin: 0 4px;
- }
- }
+.footer {
+ display: flex;
+ flex-wrap: wrap;
+ flex-direction: row;
+ justify-content: right;
- > .ok {
- margin-left: auto;
- }
+ &Item {
+ margin: 4px;
}
}
</style>
diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts
index 7bacfbdf00..de52f30523 100644
--- a/packages/client/src/scripts/initialize-sw.ts
+++ b/packages/client/src/scripts/initialize-sw.ts
@@ -1,6 +1,3 @@
-import { instance } from '@/instance';
-import { $i } from '@/account';
-import { api } from '@/os';
import { lang } from '@/config';
export async function initializeSw() {
@@ -12,57 +9,5 @@ export async function initializeSw() {
msg: 'initialize',
lang,
});
-
- if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
- // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
- registration.pushManager.subscribe({
- userVisibleOnly: true,
- applicationServerKey: urlBase64ToUint8Array(instance.swPublickey)
- })
- .then(subscription => {
- function encode(buffer: ArrayBuffer | null) {
- return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
- }
-
- // Register
- api('sw/register', {
- endpoint: subscription.endpoint,
- auth: encode(subscription.getKey('auth')),
- publickey: encode(subscription.getKey('p256dh'))
- });
- })
- // When subscribe failed
- .catch(async (err: Error) => {
- // 通知が許可されていなかったとき
- if (err.name === 'NotAllowedError') {
- return;
- }
-
- // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
- // 既に存在していることが原因でエラーになった可能性があるので、
- // そのサブスクリプションを解除しておく
- const subscription = await registration.pushManager.getSubscription();
- if (subscription) subscription.unsubscribe();
- });
- }
});
}
-
-/**
- * Convert the URL safe base64 string to a Uint8Array
- * @param base64String base64 string
- */
-function urlBase64ToUint8Array(base64String: string): Uint8Array {
- const padding = '='.repeat((4 - base64String.length % 4) % 4);
- const base64 = (base64String + padding)
- .replace(/-/g, '+')
- .replace(/_/g, '/');
-
- const rawData = window.atob(base64);
- const outputArray = new Uint8Array(rawData.length);
-
- for (let i = 0; i < rawData.length; ++i) {
- outputArray[i] = rawData.charCodeAt(i);
- }
- return outputArray;
-}