diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2022-05-01 15:08:25 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2022-05-01 15:08:25 +0900 |
| commit | c5048ee9935869e793bc941fda326d83d18ebbe8 (patch) | |
| tree | 5c1df069741ecbd3548a702da683baf53d3bae5c /packages/client/src | |
| parent | ressurect deepcopy (diff) | |
| parent | refactor(client): refactor import-export to use Composition API (#8579) (diff) | |
| download | sharkey-c5048ee9935869e793bc941fda326d83d18ebbe8.tar.gz sharkey-c5048ee9935869e793bc941fda326d83d18ebbe8.tar.bz2 sharkey-c5048ee9935869e793bc941fda326d83d18ebbe8.zip | |
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/client/src')
| -rw-r--r-- | packages/client/src/components/notification.vue | 6 | ||||
| -rw-r--r-- | packages/client/src/components/notifications.vue | 25 | ||||
| -rw-r--r-- | packages/client/src/components/ui/pagination.vue | 1 | ||||
| -rw-r--r-- | packages/client/src/init.ts | 1 | ||||
| -rw-r--r-- | packages/client/src/pages/api-console.vue | 96 | ||||
| -rw-r--r-- | packages/client/src/pages/scratchpad.vue | 163 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/import-export.vue | 154 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/instance-mute.vue | 76 | ||||
| -rw-r--r-- | packages/client/src/pages/settings/integration.vue | 167 | ||||
| -rw-r--r-- | packages/client/src/scripts/get-user-name.ts | 3 | ||||
| -rw-r--r-- | packages/client/src/scripts/initialize-sw.ts | 30 | ||||
| -rw-r--r-- | packages/client/src/sw/compose-notification.ts | 107 | ||||
| -rw-r--r-- | packages/client/src/sw/sw.ts | 123 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/common.vue | 6 | ||||
| -rw-r--r-- | packages/client/src/ui/_common_/sw-inject.ts | 45 |
15 files changed, 376 insertions, 627 deletions
diff --git a/packages/client/src/components/notification.vue b/packages/client/src/components/notification.vue index 1a360f9905..3791c576ee 100644 --- a/packages/client/src/components/notification.vue +++ b/packages/client/src/components/notification.vue @@ -72,7 +72,7 @@ </template> <script lang="ts"> -import { defineComponent, ref, onMounted, onUnmounted } from 'vue'; +import { defineComponent, ref, onMounted, onUnmounted, watch } from 'vue'; import * as misskey from 'misskey-js'; import { getNoteSummary } from '@/scripts/get-note-summary'; import XReactionIcon from './reaction-icon.vue'; @@ -126,6 +126,10 @@ export default defineComponent({ const connection = stream.useChannel('main'); connection.on('readAllNotifications', () => readObserver.disconnect()); + watch(props.notification.isRead, () => { + readObserver.disconnect(); + }); + onUnmounted(() => { readObserver.disconnect(); connection.dispose(); diff --git a/packages/client/src/components/notifications.vue b/packages/client/src/components/notifications.vue index d522503a14..dc900a670d 100644 --- a/packages/client/src/components/notifications.vue +++ b/packages/client/src/components/notifications.vue @@ -64,6 +64,31 @@ const onNotification = (notification) => { onMounted(() => { const connection = stream.useChannel('main'); connection.on('notification', onNotification); + connection.on('readAllNotifications', () => { + if (pagingComponent.value) { + for (const item of pagingComponent.value.queue) { + item.isRead = true; + } + for (const item of pagingComponent.value.items) { + item.isRead = true; + } + } + }); + connection.on('readNotifications', notificationIds => { + if (pagingComponent.value) { + for (let i = 0; i < pagingComponent.value.queue.length; i++) { + if (notificationIds.includes(pagingComponent.value.queue[i].id)) { + pagingComponent.value.queue[i].isRead = true; + } + } + for (let i = 0; i < (pagingComponent.value.items || []).length; i++) { + if (notificationIds.includes(pagingComponent.value.items[i].id)) { + pagingComponent.value.items[i].isRead = true; + } + } + } + }); + onUnmounted(() => { connection.dispose(); }); diff --git a/packages/client/src/components/ui/pagination.vue b/packages/client/src/components/ui/pagination.vue index 13f3215671..ac6f59c332 100644 --- a/packages/client/src/components/ui/pagination.vue +++ b/packages/client/src/components/ui/pagination.vue @@ -270,6 +270,7 @@ onDeactivated(() => { defineExpose({ items, + queue, backed, reload, fetchMoreAhead, diff --git a/packages/client/src/init.ts b/packages/client/src/init.ts index 5809f25689..58fdd25835 100644 --- a/packages/client/src/init.ts +++ b/packages/client/src/init.ts @@ -149,7 +149,6 @@ if ($i && $i.token) { try { document.body.innerHTML = '<div>Please wait...</div>'; await login(i); - location.reload(); } catch (e) { // Render the error screen // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) diff --git a/packages/client/src/pages/api-console.vue b/packages/client/src/pages/api-console.vue index 142a3bee2e..7f174a6318 100644 --- a/packages/client/src/pages/api-console.vue +++ b/packages/client/src/pages/api-console.vue @@ -25,8 +25,8 @@ </MkSpacer> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref } from 'vue'; import * as JSON5 from 'json5'; import MkButton from '@/components/ui/button.vue'; import MkInput from '@/components/form/input.vue'; @@ -34,63 +34,51 @@ import MkTextarea from '@/components/form/textarea.vue'; import MkSwitch from '@/components/form/switch.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { Endpoints } from 'misskey-js'; -export default defineComponent({ - components: { - MkButton, MkInput, MkTextarea, MkSwitch, - }, +const body = ref('{}'); +const endpoint = ref(''); +const endpoints = ref<any[]>([]); +const sending = ref(false); +const res = ref(''); +const withCredential = ref(true); - data() { - return { - [symbols.PAGE_INFO]: { - title: 'API console', - icon: 'fas fa-terminal' - }, +os.api('endpoints').then(endpointResponse => { + endpoints.value = endpointResponse; +}); - endpoint: '', - body: '{}', - res: null, - sending: false, - endpoints: [], - withCredential: true, +function send() { + sending.value = true; + const requestBody = JSON5.parse(body.value); + os.api(endpoint.value as keyof Endpoints, requestBody, requestBody.i || (withCredential.value ? undefined : null)).then(resp => { + sending.value = false; + res.value = JSON5.stringify(resp, null, 2); + }, err => { + sending.value = false; + res.value = JSON5.stringify(err, null, 2); + }); +} - }; - }, +function onEndpointChange() { + os.api('endpoint', { endpoint: endpoint.value }, withCredential.value ? undefined : null).then(resp => { + const endpointBody = {}; + for (const p of resp.params) { + endpointBody[p.name] = + p.type === 'String' ? '' : + p.type === 'Number' ? 0 : + p.type === 'Boolean' ? false : + p.type === 'Array' ? [] : + p.type === 'Object' ? {} : + null; + } + body.value = JSON5.stringify(endpointBody, null, 2); + }); +} - created() { - os.api('endpoints').then(endpoints => { - this.endpoints = endpoints; - }); +defineExpose({ + [symbols.PAGE_INFO]: { + title: 'API console', + icon: 'fas fa-terminal' }, - - methods: { - send() { - this.sending = true; - const body = JSON5.parse(this.body); - os.api(this.endpoint, body, body.i || (this.withCredential ? undefined : null)).then(res => { - this.sending = false; - this.res = JSON5.stringify(res, null, 2); - }, err => { - this.sending = false; - this.res = JSON5.stringify(err, null, 2); - }); - }, - - onEndpointChange() { - os.api('endpoint', { endpoint: this.endpoint }, this.withCredential ? undefined : null).then(endpoint => { - const body = {}; - for (const p of endpoint.params) { - body[p.name] = - p.type === 'String' ? '' : - p.type === 'Number' ? 0 : - p.type === 'Boolean' ? false : - p.type === 'Array' ? [] : - p.type === 'Object' ? {} : - null; - } - this.body = JSON5.stringify(body, null, 2); - }); - } - } }); </script> diff --git a/packages/client/src/pages/scratchpad.vue b/packages/client/src/pages/scratchpad.vue index f871dc48e8..eb91938db2 100644 --- a/packages/client/src/pages/scratchpad.vue +++ b/packages/client/src/pages/scratchpad.vue @@ -6,20 +6,20 @@ </div> <MkContainer :foldable="true" class="_gap"> - <template #header>{{ $ts.output }}</template> + <template #header>{{ i18n.ts.output }}</template> <div class="bepmlvbi"> <div v-for="log in logs" :key="log.id" class="log" :class="{ print: log.print }">{{ log.text }}</div> </div> </MkContainer> <div class="_gap"> - {{ $ts.scratchpadDescription }} + {{ i18n.ts.scratchpadDescription }} </div> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref, watch } from 'vue'; import 'prismjs'; import { highlight, languages } from 'prismjs/components/prism-core'; import 'prismjs/components/prism-clike'; @@ -27,103 +27,90 @@ import 'prismjs/components/prism-javascript'; import 'prismjs/themes/prism-okaidia.css'; import { PrismEditor } from 'vue-prism-editor'; import 'vue-prism-editor/dist/prismeditor.min.css'; -import { AiScript, parse, utils, values } from '@syuilo/aiscript'; +import { AiScript, parse, utils } from '@syuilo/aiscript'; import MkContainer from '@/components/ui/container.vue'; import MkButton from '@/components/ui/button.vue'; import { createAiScriptEnv } from '@/scripts/aiscript/api'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkContainer, - MkButton, - PrismEditor, - }, - - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.scratchpad, - icon: 'fas fa-terminal', - }, - code: '', - logs: [], - } - }, - - watch: { - code() { - localStorage.setItem('scratchpad', this.code); - } - }, +const code = ref(''); +const logs = ref<any[]>([]); - created() { - const saved = localStorage.getItem('scratchpad'); - if (saved) { - this.code = saved; - } - }, +const saved = localStorage.getItem('scratchpad'); +if (saved) { + code.value = saved; +} - methods: { - async run() { - this.logs = []; - const aiscript = new AiScript(createAiScriptEnv({ - storageKey: 'scratchpad', - token: this.$i?.token, - }), { - in: (q) => { - return new Promise(ok => { - os.inputText({ - title: q, - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - this.logs.push({ - id: Math.random(), - text: value.type === 'str' ? value.value : utils.valToString(value), - print: true - }); - }, - log: (type, params) => { - switch (type) { - case 'end': this.logs.push({ - id: Math.random(), - text: utils.valToString(params.val, true), - print: false - }); break; - default: break; - } - } - }); +watch(code, () => { + localStorage.setItem('scratchpad', code.value); +}); - let ast; - try { - ast = parse(this.code); - } catch (e) { - os.alert({ - type: 'error', - text: 'Syntax error :(' - }); - return; - } - try { - await aiscript.exec(ast); - } catch (e) { - os.alert({ - type: 'error', - text: e +async function run() { + logs.value = []; + const aiscript = new AiScript(createAiScriptEnv({ + storageKey: 'scratchpad', + token: $i?.token, + }), { + in: (q) => { + return new Promise(ok => { + os.inputText({ + title: q, + }).then(({ canceled, result: a }) => { + ok(a); }); - } + }); }, - - highlighter(code) { - return highlight(code, languages.js, 'javascript'); + out: (value) => { + logs.value.push({ + id: Math.random(), + text: value.type === 'str' ? value.value : utils.valToString(value), + print: true + }); }, + log: (type, params) => { + switch (type) { + case 'end': logs.value.push({ + id: Math.random(), + text: utils.valToString(params.val, true), + print: false + }); break; + default: break; + } + } + }); + + let ast; + try { + ast = parse(code.value); + } catch (error) { + os.alert({ + type: 'error', + text: 'Syntax error :(' + }); + return; + } + try { + await aiscript.exec(ast); + } catch (error: any) { + os.alert({ + type: 'error', + text: error.message + }); } +}; + +function highlighter(code) { + return highlight(code, languages.js, 'javascript'); +} + +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.scratchpad, + icon: 'fas fa-terminal', + }, }); </script> diff --git a/packages/client/src/pages/settings/import-export.vue b/packages/client/src/pages/settings/import-export.vue index c153b4d28c..127cbcd4c1 100644 --- a/packages/client/src/pages/settings/import-export.vue +++ b/packages/client/src/pages/settings/import-export.vue @@ -37,8 +37,8 @@ </div> </template> -<script lang="ts"> -import { defineComponent, onMounted, ref } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref } from 'vue'; import MkButton from '@/components/ui/button.vue'; import FormSection from '@/components/form/section.vue'; import FormGroup from '@/components/form/group.vue'; @@ -48,108 +48,80 @@ import { selectFile } from '@/scripts/select-file'; import * as symbols from '@/symbols'; import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - FormGroup, - FormSwitch, - MkButton, - }, +const excludeMutingUsers = ref(false); +const excludeInactiveUsers = ref(false); - emits: ['info'], +const onExportSuccess = () => { + os.alert({ + type: 'info', + text: i18n.ts.exportRequested, + }); +}; - setup(props, context) { - const INFO = { - title: i18n.ts.importAndExport, - icon: 'fas fa-boxes', - bg: 'var(--bg)', - }; +const onImportSuccess = () => { + os.alert({ + type: 'info', + text: i18n.ts.importRequested, + }); +}; - const excludeMutingUsers = ref(false); - const excludeInactiveUsers = ref(false); +const onError = (ev) => { + os.alert({ + type: 'error', + text: ev.message, + }); +}; - const onExportSuccess = () => { - os.alert({ - type: 'info', - text: i18n.ts.exportRequested, - }); - }; +const exportNotes = () => { + os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); +}; - const onImportSuccess = () => { - os.alert({ - type: 'info', - text: i18n.ts.importRequested, - }); - }; +const exportFollowing = () => { + os.api('i/export-following', { + excludeMuting: excludeMutingUsers.value, + excludeInactive: excludeInactiveUsers.value, + }) + .then(onExportSuccess).catch(onError); +}; - const onError = (e) => { - os.alert({ - type: 'error', - text: e.message, - }); - }; +const exportBlocking = () => { + os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); +}; - const exportNotes = () => { - os.api('i/export-notes', {}).then(onExportSuccess).catch(onError); - }; +const exportUserLists = () => { + os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); +}; - const exportFollowing = () => { - os.api('i/export-following', { - excludeMuting: excludeMutingUsers.value, - excludeInactive: excludeInactiveUsers.value, - }) - .then(onExportSuccess).catch(onError); - }; +const exportMuting = () => { + os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); +}; - const exportBlocking = () => { - os.api('i/export-blocking', {}).then(onExportSuccess).catch(onError); - }; +const importFollowing = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const exportUserLists = () => { - os.api('i/export-user-lists', {}).then(onExportSuccess).catch(onError); - }; +const importUserLists = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const exportMuting = () => { - os.api('i/export-mute', {}).then(onExportSuccess).catch(onError); - }; +const importMuting = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const importFollowing = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-following', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; +const importBlocking = async (ev) => { + const file = await selectFile(ev.currentTarget ?? ev.target); + os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); +}; - const importUserLists = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; - - const importMuting = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; - - const importBlocking = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); - os.api('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); - }; - - return { - [symbols.PAGE_INFO]: INFO, - excludeMutingUsers, - excludeInactiveUsers, - - exportNotes, - exportFollowing, - exportBlocking, - exportUserLists, - exportMuting, - - importFollowing, - importUserLists, - importMuting, - importBlocking, - }; - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.importAndExport, + icon: 'fas fa-boxes', + bg: 'var(--bg)', + } }); </script> diff --git a/packages/client/src/pages/settings/instance-mute.vue b/packages/client/src/pages/settings/instance-mute.vue index f84a209b60..bcc2ee85ad 100644 --- a/packages/client/src/pages/settings/instance-mute.vue +++ b/packages/client/src/pages/settings/instance-mute.vue @@ -1,67 +1,51 @@ <template> <div class="_formRoot"> - <MkInfo>{{ $ts._instanceMute.title }}</MkInfo> + <MkInfo>{{ i18n.ts._instanceMute.title }}</MkInfo> <FormTextarea v-model="instanceMutes" class="_formBlock"> - <template #label>{{ $ts._instanceMute.heading }}</template> - <template #caption>{{ $ts._instanceMute.instanceMuteDescription }}<br>{{ $ts._instanceMute.instanceMuteDescription2 }}</template> + <template #label>{{ i18n.ts._instanceMute.heading }}</template> + <template #caption>{{ i18n.ts._instanceMute.instanceMuteDescription }}<br>{{ i18n.ts._instanceMute.instanceMuteDescription2 }}</template> </FormTextarea> - <MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ $ts.save }}</MkButton> + <MkButton primary :disabled="!changed" class="_formBlock" @click="save()"><i class="fas fa-save"></i> {{ i18n.ts.save }}</MkButton> </div> </template> -<script> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { defineExpose, ref, watch } from 'vue'; import FormTextarea from '@/components/form/textarea.vue'; import MkInfo from '@/components/ui/info.vue'; import MkButton from '@/components/ui/button.vue'; import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - MkButton, - FormTextarea, - MkInfo, - }, +const instanceMutes = ref($i!.mutedInstances.join('\n')); +const changed = ref(false); - emits: ['info'], +async function save() { + let mutes = instanceMutes.value + .trim().split('\n') + .map(el => el.trim()) + .filter(el => el); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.instanceMute, - icon: 'fas fa-volume-mute' - }, - tab: 'soft', - instanceMutes: '', - changed: false, - } - }, + await os.api('i/update', { + mutedInstances: mutes, + }); - watch: { - instanceMutes: { - handler() { - this.changed = true; - }, - deep: true - }, - }, + changed.value = false; - async created() { - this.instanceMutes = this.$i.mutedInstances.join('\n'); - }, + // Refresh filtered list to signal to the user how they've been saved + instanceMutes.value = mutes.join('\n'); +} - methods: { - async save() { - let mutes = this.instanceMutes.trim().split('\n').map(el => el.trim()).filter(el => el); - await os.api('i/update', { - mutedInstances: mutes, - }); - this.changed = false; +watch(instanceMutes, () => { + changed.value = true; +}); - // Refresh filtered list to signal to the user how they've been saved - this.instanceMutes = mutes.join('\n'); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.instanceMute, + icon: 'fas fa-volume-mute' } -}) +}); </script> diff --git a/packages/client/src/pages/settings/integration.vue b/packages/client/src/pages/settings/integration.vue index ca36c91665..75c6200944 100644 --- a/packages/client/src/pages/settings/integration.vue +++ b/packages/client/src/pages/settings/integration.vue @@ -1,133 +1,98 @@ <template> <div class="_formRoot"> - <FormSection v-if="enableTwitterIntegration"> + <FormSection v-if="instance.enableTwitterIntegration"> <template #label><i class="fab fa-twitter"></i> Twitter</template> - <p v-if="integrations.twitter">{{ $ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p> - <MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectTwitter">{{ $ts.connectService }}</MkButton> + <p v-if="integrations.twitter">{{ i18n.ts.connectedTo }}: <a :href="`https://twitter.com/${integrations.twitter.screenName}`" rel="nofollow noopener" target="_blank">@{{ integrations.twitter.screenName }}</a></p> + <MkButton v-if="integrations.twitter" danger @click="disconnectTwitter">{{ i18n.ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectTwitter">{{ i18n.ts.connectService }}</MkButton> </FormSection> - <FormSection v-if="enableDiscordIntegration"> + <FormSection v-if="instance.enableDiscordIntegration"> <template #label><i class="fab fa-discord"></i> Discord</template> - <p v-if="integrations.discord">{{ $ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p> - <MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectDiscord">{{ $ts.connectService }}</MkButton> + <p v-if="integrations.discord">{{ i18n.ts.connectedTo }}: <a :href="`https://discord.com/users/${integrations.discord.id}`" rel="nofollow noopener" target="_blank">@{{ integrations.discord.username }}#{{ integrations.discord.discriminator }}</a></p> + <MkButton v-if="integrations.discord" danger @click="disconnectDiscord">{{ i18n.ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectDiscord">{{ i18n.ts.connectService }}</MkButton> </FormSection> - <FormSection v-if="enableGithubIntegration"> + <FormSection v-if="instance.enableGithubIntegration"> <template #label><i class="fab fa-github"></i> GitHub</template> - <p v-if="integrations.github">{{ $ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p> - <MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ $ts.disconnectService }}</MkButton> - <MkButton v-else primary @click="connectGithub">{{ $ts.connectService }}</MkButton> + <p v-if="integrations.github">{{ i18n.ts.connectedTo }}: <a :href="`https://github.com/${integrations.github.login}`" rel="nofollow noopener" target="_blank">@{{ integrations.github.login }}</a></p> + <MkButton v-if="integrations.github" danger @click="disconnectGithub">{{ i18n.ts.disconnectService }}</MkButton> + <MkButton v-else primary @click="connectGithub">{{ i18n.ts.connectService }}</MkButton> </FormSection> </div> </template> -<script lang="ts"> -import { defineComponent } from 'vue'; +<script lang="ts" setup> +import { computed, defineExpose, onMounted, ref, watch } from 'vue'; import { apiUrl } from '@/config'; import FormSection from '@/components/form/section.vue'; import MkButton from '@/components/ui/button.vue'; -import * as os from '@/os'; import * as symbols from '@/symbols'; +import { $i } from '@/account'; +import { instance } from '@/instance'; +import { i18n } from '@/i18n'; -export default defineComponent({ - components: { - FormSection, - MkButton - }, +const twitterForm = ref<Window | null>(null); +const discordForm = ref<Window | null>(null); +const githubForm = ref<Window | null>(null); - emits: ['info'], +const integrations = computed(() => $i!.integrations); - data() { - return { - [symbols.PAGE_INFO]: { - title: this.$ts.integration, - icon: 'fas fa-share-alt', - bg: 'var(--bg)', - }, - apiUrl, - twitterForm: null, - discordForm: null, - githubForm: null, - enableTwitterIntegration: false, - enableDiscordIntegration: false, - enableGithubIntegration: false, - }; - }, +function openWindow(service: string, type: string) { + return window.open(`${apiUrl}/${type}/${service}`, + `${service}_${type}_window`, + 'height=570, width=520' + ); +} - computed: { - integrations() { - return this.$i.integrations; - }, - - meta() { - return this.$instance; - }, - }, +function connectTwitter() { + twitterForm.value = openWindow('twitter', 'connect'); +} - created() { - this.enableTwitterIntegration = this.meta.enableTwitterIntegration; - this.enableDiscordIntegration = this.meta.enableDiscordIntegration; - this.enableGithubIntegration = this.meta.enableGithubIntegration; - }, +function disconnectTwitter() { + openWindow('twitter', 'disconnect'); +} - mounted() { - document.cookie = `igi=${this.$i.token}; path=/;` + - ` max-age=31536000;` + - (document.location.protocol.startsWith('https') ? ' secure' : ''); +function connectDiscord() { + discordForm.value = openWindow('discord', 'connect'); +} - this.$watch('integrations', () => { - if (this.integrations.twitter) { - if (this.twitterForm) this.twitterForm.close(); - } - if (this.integrations.discord) { - if (this.discordForm) this.discordForm.close(); - } - if (this.integrations.github) { - if (this.githubForm) this.githubForm.close(); - } - }, { - deep: true - }); - }, +function disconnectDiscord() { + openWindow('discord', 'disconnect'); +} - methods: { - connectTwitter() { - this.twitterForm = window.open(apiUrl + '/connect/twitter', - 'twitter_connect_window', - 'height=570, width=520'); - }, +function connectGithub() { + githubForm.value = openWindow('github', 'connect'); +} - disconnectTwitter() { - window.open(apiUrl + '/disconnect/twitter', - 'twitter_disconnect_window', - 'height=570, width=520'); - }, +function disconnectGithub() { + openWindow('github', 'disconnect'); +} - connectDiscord() { - this.discordForm = window.open(apiUrl + '/connect/discord', - 'discord_connect_window', - 'height=570, width=520'); - }, +onMounted(() => { + document.cookie = `igi=${$i!.token}; path=/;` + + ` max-age=31536000;` + + (document.location.protocol.startsWith('https') ? ' secure' : ''); - disconnectDiscord() { - window.open(apiUrl + '/disconnect/discord', - 'discord_disconnect_window', - 'height=570, width=520'); - }, - - connectGithub() { - this.githubForm = window.open(apiUrl + '/connect/github', - 'github_connect_window', - 'height=570, width=520'); - }, + watch(integrations, () => { + if (integrations.value.twitter) { + if (twitterForm.value) twitterForm.value.close(); + } + if (integrations.value.discord) { + if (discordForm.value) discordForm.value.close(); + } + if (integrations.value.github) { + if (githubForm.value) githubForm.value.close(); + } + }); +}); - disconnectGithub() { - window.open(apiUrl + '/disconnect/github', - 'github_disconnect_window', - 'height=570, width=520'); - }, +defineExpose({ + [symbols.PAGE_INFO]: { + title: i18n.ts.integration, + icon: 'fas fa-share-alt', + bg: 'var(--bg)', } }); </script> diff --git a/packages/client/src/scripts/get-user-name.ts b/packages/client/src/scripts/get-user-name.ts new file mode 100644 index 0000000000..d499ea0203 --- /dev/null +++ b/packages/client/src/scripts/get-user-name.ts @@ -0,0 +1,3 @@ +export default function(user: { name?: string | null, username: string }): string { + return user.name || user.username; +} diff --git a/packages/client/src/scripts/initialize-sw.ts b/packages/client/src/scripts/initialize-sw.ts index d6dbd5dbd4..7bacfbdf00 100644 --- a/packages/client/src/scripts/initialize-sw.ts +++ b/packages/client/src/scripts/initialize-sw.ts @@ -4,26 +4,26 @@ import { api } from '@/os'; import { lang } from '@/config'; export async function initializeSw() { - if (instance.swPublickey && - ('serviceWorker' in navigator) && - ('PushManager' in window) && - $i && $i.token) { - navigator.serviceWorker.register(`/sw.js`); + if (!('serviceWorker' in navigator)) return; - navigator.serviceWorker.ready.then(registration => { - registration.active?.postMessage({ - msg: 'initialize', - lang, - }); + navigator.serviceWorker.register(`/sw.js`, { scope: '/', type: 'classic' }); + navigator.serviceWorker.ready.then(registration => { + registration.active?.postMessage({ + 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 => { + }) + .then(subscription => { function encode(buffer: ArrayBuffer | null) { return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer))); } - + // Register api('sw/register', { endpoint: subscription.endpoint, @@ -37,15 +37,15 @@ export async function initializeSw() { if (err.name === 'NotAllowedError') { return; } - + // 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが // 既に存在していることが原因でエラーになった可能性があるので、 // そのサブスクリプションを解除しておく const subscription = await registration.pushManager.getSubscription(); if (subscription) subscription.unsubscribe(); }); - }); - } + } + }); } /** diff --git a/packages/client/src/sw/compose-notification.ts b/packages/client/src/sw/compose-notification.ts deleted file mode 100644 index e271d30949..0000000000 --- a/packages/client/src/sw/compose-notification.ts +++ /dev/null @@ -1,107 +0,0 @@ -/** - * Notification composer of Service Worker - */ -declare var self: ServiceWorkerGlobalScope; - -import * as misskey from 'misskey-js'; - -function getUserName(user: misskey.entities.User): string { - return user.name || user.username; -} - -export default async function(type, data, i18n): Promise<[string, NotificationOptions] | null | undefined> { - if (!i18n) { - console.log('no i18n'); - return; - } - - switch (type) { - case 'driveFileCreated': // TODO (Server Side) - return [i18n.t('_notification.fileUploaded'), { - body: data.name, - icon: data.url - }]; - case 'notification': - switch (data.type) { - case 'mention': - return [i18n.t('_notification.youGotMention', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'reply': - return [i18n.t('_notification.youGotReply', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'renote': - return [i18n.t('_notification.youRenoted', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'quote': - return [i18n.t('_notification.youGotQuote', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'reaction': - return [`${data.reaction} ${getUserName(data.user)}`, { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'pollVote': - return [i18n.t('_notification.youGotPoll', { name: getUserName(data.user) }), { - body: data.note.text, - icon: data.user.avatarUrl - }]; - - case 'pollEnded': - return [i18n.t('_notification.pollEnded'), { - body: data.note.text, - }]; - - case 'follow': - return [i18n.t('_notification.youWereFollowed'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'receiveFollowRequest': - return [i18n.t('_notification.youReceivedFollowRequest'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'followRequestAccepted': - return [i18n.t('_notification.yourFollowRequestAccepted'), { - body: getUserName(data.user), - icon: data.user.avatarUrl - }]; - - case 'groupInvited': - return [i18n.t('_notification.youWereInvitedToGroup'), { - body: data.group.name - }]; - - default: - return null; - } - case 'unreadMessagingMessage': - if (data.groupId === null) { - return [i18n.t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.user) }), { - icon: data.user.avatarUrl, - tag: `messaging:user:${data.user.id}` - }]; - } - return [i18n.t('_notification.youGotMessagingMessageFromGroup', { name: data.group.name }), { - icon: data.user.avatarUrl, - tag: `messaging:group:${data.group.id}` - }]; - default: - return null; - } -} diff --git a/packages/client/src/sw/sw.ts b/packages/client/src/sw/sw.ts deleted file mode 100644 index 68c650c771..0000000000 --- a/packages/client/src/sw/sw.ts +++ /dev/null @@ -1,123 +0,0 @@ -/** - * Service Worker - */ -declare var self: ServiceWorkerGlobalScope; - -import { get, set } from 'idb-keyval'; -import composeNotification from '@/sw/compose-notification'; -import { I18n } from '@/scripts/i18n'; - -//#region Variables -const version = _VERSION_; -const cacheName = `mk-cache-${version}`; - -let lang: string; -let i18n: I18n<any>; -let pushesPool: any[] = []; -//#endregion - -//#region Startup -get('lang').then(async prelang => { - if (!prelang) return; - lang = prelang; - return fetchLocale(); -}); -//#endregion - -//#region Lifecycle: Install -self.addEventListener('install', ev => { - self.skipWaiting(); -}); -//#endregion - -//#region Lifecycle: Activate -self.addEventListener('activate', ev => { - ev.waitUntil( - caches.keys() - .then(cacheNames => Promise.all( - cacheNames - .filter((v) => v !== cacheName) - .map(name => caches.delete(name)) - )) - .then(() => self.clients.claim()) - ); -}); -//#endregion - -//#region When: Fetching -self.addEventListener('fetch', ev => { - // Nothing to do -}); -//#endregion - -//#region When: Caught Notification -self.addEventListener('push', ev => { - // クライアント取得 - ev.waitUntil(self.clients.matchAll({ - includeUncontrolled: true - }).then(async clients => { - // クライアントがあったらストリームに接続しているということなので通知しない - if (clients.length != 0) return; - - const { type, body } = ev.data?.json(); - - // localeを読み込めておらずi18nがundefinedだった場合はpushesPoolにためておく - if (!i18n) return pushesPool.push({ type, body }); - - const n = await composeNotification(type, body, i18n); - if (n) return self.registration.showNotification(...n); - })); -}); -//#endregion - -//#region When: Caught a message from the client -self.addEventListener('message', ev => { - switch(ev.data) { - case 'clear': - return; // TODO - default: - break; - } - - if (typeof ev.data === 'object') { - // E.g. '[object Array]' → 'array' - const otype = Object.prototype.toString.call(ev.data).slice(8, -1).toLowerCase(); - - if (otype === 'object') { - if (ev.data.msg === 'initialize') { - lang = ev.data.lang; - set('lang', lang); - fetchLocale(); - } - } - } -}); -//#endregion - -//#region Function: (Re)Load i18n instance -async function fetchLocale() { - //#region localeファイルの読み込み - // Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う - const localeUrl = `/assets/locales/${lang}.${version}.json`; - let localeRes = await caches.match(localeUrl); - - if (!localeRes) { - localeRes = await fetch(localeUrl); - const clone = localeRes?.clone(); - if (!clone?.clone().ok) return; - - caches.open(cacheName).then(cache => cache.put(localeUrl, clone)); - } - - i18n = new I18n(await localeRes.json()); - //#endregion - - //#region i18nをきちんと読み込んだ後にやりたい処理 - for (const { type, body } of pushesPool) { - const n = await composeNotification(type, body, i18n); - if (n) self.registration.showNotification(...n); - } - pushesPool = []; - //#endregion -} -//#endregion diff --git a/packages/client/src/ui/_common_/common.vue b/packages/client/src/ui/_common_/common.vue index 50d95539d1..62e97a11e1 100644 --- a/packages/client/src/ui/_common_/common.vue +++ b/packages/client/src/ui/_common_/common.vue @@ -21,6 +21,7 @@ import { popup, popups, pendingApiRequestsCount } from '@/os'; import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; +import { swInject } from './sw-inject'; import { stream } from '@/stream'; export default defineComponent({ @@ -49,6 +50,11 @@ export default defineComponent({ if ($i) { const connection = stream.useChannel('main', null, 'UI'); connection.on('notification', onNotification); + + //#region Listen message from SW + if ('serviceWorker' in navigator) { + swInject(); + } } return { diff --git a/packages/client/src/ui/_common_/sw-inject.ts b/packages/client/src/ui/_common_/sw-inject.ts new file mode 100644 index 0000000000..e3e2ddd7e6 --- /dev/null +++ b/packages/client/src/ui/_common_/sw-inject.ts @@ -0,0 +1,45 @@ +import { inject } from 'vue'; +import { post } from '@/os'; +import { $i, login } from '@/account'; +import { defaultStore } from '@/store'; +import { getAccountFromId } from '@/scripts/get-account-from-id'; +import { router } from '@/router'; + +export function swInject() { + const navHook = inject('navHook', null); + const sideViewHook = inject('sideViewHook', null); + + navigator.serviceWorker.addEventListener('message', ev => { + if (_DEV_) { + console.log('sw msg', ev.data); + } + + const data = ev.data; // as SwMessage + if (data.type !== 'order') return; + + if (data.loginId !== $i?.id) { + return getAccountFromId(data.loginId).then(account => { + if (!account) return; + return login(account.token, data.url); + }); + } + + switch (data.order) { + case 'post': + return post(data.options); + case 'push': + if (router.currentRoute.value.path === data.url) { + return window.scroll({ top: 0, behavior: 'smooth' }); + } + if (navHook) { + return navHook(data.url); + } + if (sideViewHook && defaultStore.state.defaultSideView && data.url !== '/') { + return sideViewHook(data.url); + } + return router.push(data.url); + default: + return; + } + }); +} |