summaryrefslogtreecommitdiff
path: root/packages/client/src
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-05-01 15:08:25 +0900
committerGitHub <noreply@github.com>2022-05-01 15:08:25 +0900
commitc5048ee9935869e793bc941fda326d83d18ebbe8 (patch)
tree5c1df069741ecbd3548a702da683baf53d3bae5c /packages/client/src
parentressurect deepcopy (diff)
parentrefactor(client): refactor import-export to use Composition API (#8579) (diff)
downloadsharkey-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.vue6
-rw-r--r--packages/client/src/components/notifications.vue25
-rw-r--r--packages/client/src/components/ui/pagination.vue1
-rw-r--r--packages/client/src/init.ts1
-rw-r--r--packages/client/src/pages/api-console.vue96
-rw-r--r--packages/client/src/pages/scratchpad.vue163
-rw-r--r--packages/client/src/pages/settings/import-export.vue154
-rw-r--r--packages/client/src/pages/settings/instance-mute.vue76
-rw-r--r--packages/client/src/pages/settings/integration.vue167
-rw-r--r--packages/client/src/scripts/get-user-name.ts3
-rw-r--r--packages/client/src/scripts/initialize-sw.ts30
-rw-r--r--packages/client/src/sw/compose-notification.ts107
-rw-r--r--packages/client/src/sw/sw.ts123
-rw-r--r--packages/client/src/ui/_common_/common.vue6
-rw-r--r--packages/client/src/ui/_common_/sw-inject.ts45
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;
+ }
+ });
+}