✌️
This commit is contained in:
parent
a8c4c74954
commit
bf1db27824
|
@ -1567,7 +1567,7 @@ _notification:
|
||||||
youWereFollowed: "フォローされました"
|
youWereFollowed: "フォローされました"
|
||||||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||||
youWereInvitedToGroup: "グループに招待されました"
|
youWereInvitedToGroup: "{userName}があなたをグループに招待しました"
|
||||||
|
|
||||||
_types:
|
_types:
|
||||||
all: "すべて"
|
all: "すべて"
|
||||||
|
@ -1583,6 +1583,11 @@ _notification:
|
||||||
groupInvited: "グループに招待された"
|
groupInvited: "グループに招待された"
|
||||||
app: "連携アプリからの通知"
|
app: "連携アプリからの通知"
|
||||||
|
|
||||||
|
_actions:
|
||||||
|
followBack: "フォローバック"
|
||||||
|
reply: "返信"
|
||||||
|
renote: "Renote"
|
||||||
|
|
||||||
_deck:
|
_deck:
|
||||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||||
columnAlign: "カラムの寄せ"
|
columnAlign: "カラムの寄せ"
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { get, set } from 'idb-keyval';
|
||||||
import { reactive } from 'vue';
|
import { reactive } from 'vue';
|
||||||
import { apiUrl } from '@/config';
|
import { apiUrl } from '@/config';
|
||||||
import { waiting } from '@/os';
|
import { waiting } from '@/os';
|
||||||
import { unisonReload } from '@/scripts/unison-reload';
|
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||||
|
|
||||||
// TODO: 他のタブと永続化されたstateを同期
|
// TODO: 他のタブと永続化されたstateを同期
|
||||||
|
|
||||||
|
@ -89,18 +89,23 @@ export function updateAccount(data) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshAccount() {
|
export function refreshAccount() {
|
||||||
fetchAccount($i.token).then(updateAccount);
|
return fetchAccount($i.token).then(updateAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(token: Account['token'], showTimeline: boolean = false) {
|
export async function login(token: Account['token'], href?: string) {
|
||||||
waiting();
|
waiting();
|
||||||
if (_DEV_) console.log('logging as token ', token);
|
if (_DEV_) console.log('logging as token ', token);
|
||||||
const me = await fetchAccount(token);
|
const me = await fetchAccount(token);
|
||||||
localStorage.setItem('account', JSON.stringify(me));
|
localStorage.setItem('account', JSON.stringify(me));
|
||||||
await addAccount(me.id, token);
|
await addAccount(me.id, token);
|
||||||
|
|
||||||
if (showTimeline) location.href = '/';
|
if (href) {
|
||||||
else unisonReload();
|
reloadChannel.postMessage('reload');
|
||||||
|
location.href = href;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unisonReload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
||||||
|
|
|
@ -61,11 +61,14 @@ import * as sound from '@/scripts/sound';
|
||||||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||||
import { defaultStore, ColdDeviceStorage } from '@/store';
|
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||||
import { fetchInstance, instance } from '@/instance';
|
import { fetchInstance, instance } from '@/instance';
|
||||||
import { makeHotkey } from './scripts/hotkey';
|
import { makeHotkey } from '@/scripts/hotkey';
|
||||||
import { search } from './scripts/search';
|
import { search } from '@/scripts/search';
|
||||||
import { getThemes } from './theme-store';
|
import { getThemes } from '@/theme-store';
|
||||||
import { initializeSw } from './scripts/initialize-sw';
|
import { initializeSw } from '@/scripts/initialize-sw';
|
||||||
import { reloadChannel } from './scripts/unison-reload';
|
import { reloadChannel } from '@/scripts/unison-reload';
|
||||||
|
import { deleteLoginId } from '@/scripts/login-id';
|
||||||
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
|
import { SwMessage } from '@/sw/types';
|
||||||
|
|
||||||
console.info(`Misskey v${version}`);
|
console.info(`Misskey v${version}`);
|
||||||
|
|
||||||
|
@ -142,6 +145,25 @@ const html = document.documentElement;
|
||||||
html.setAttribute('lang', lang);
|
html.setAttribute('lang', lang);
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
|
//#region loginId
|
||||||
|
const params = new URLSearchParams(location.href);
|
||||||
|
const loginId = params.get('loginId');
|
||||||
|
|
||||||
|
if (loginId) {
|
||||||
|
const target = deleteLoginId(location.toString());
|
||||||
|
|
||||||
|
if (!$i || $i.id !== loginId) {
|
||||||
|
const account = await getAccountFromId(loginId);
|
||||||
|
if (account) {
|
||||||
|
login(account.token, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history.replaceState({ misskey: 'loginId' }, '', target)
|
||||||
|
}
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//#region Fetch user
|
//#region Fetch user
|
||||||
if ($i && $i.token) {
|
if ($i && $i.token) {
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
|
@ -188,7 +210,7 @@ fetchInstance().then(() => {
|
||||||
stream.init($i);
|
stream.init($i);
|
||||||
|
|
||||||
const app = createApp(await (
|
const app = createApp(await (
|
||||||
window.location.search === '?zen' ? import('@/ui/zen.vue') :
|
location.search === '?zen' ? import('@/ui/zen.vue') :
|
||||||
!$i ? import('@/ui/visitor.vue') :
|
!$i ? import('@/ui/visitor.vue') :
|
||||||
ui === 'deck' ? import('@/ui/deck.vue') :
|
ui === 'deck' ? import('@/ui/deck.vue') :
|
||||||
ui === 'desktop' ? import('@/ui/desktop.vue') :
|
ui === 'desktop' ? import('@/ui/desktop.vue') :
|
||||||
|
@ -217,6 +239,33 @@ components(app);
|
||||||
|
|
||||||
await router.isReady();
|
await router.isReady();
|
||||||
|
|
||||||
|
//#region Listen message from SW
|
||||||
|
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':
|
||||||
|
return router.push(data.url);
|
||||||
|
default:
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
//document.body.innerHTML = '<div id="app"></div>';
|
//document.body.innerHTML = '<div id="app"></div>';
|
||||||
|
|
||||||
app.mount('body');
|
app.mount('body');
|
||||||
|
|
7
src/client/scripts/get-account-from-id.ts
Normal file
7
src/client/scripts/get-account-from-id.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import { get } from 'idb-keyval';
|
||||||
|
|
||||||
|
export async function getAccountFromId(id: string) {
|
||||||
|
const accounts = await get('accounts') as { token: string; id: string; }[];
|
||||||
|
if (!accounts) console.log('Accounts are not recorded');
|
||||||
|
return accounts.find(e => e.id === id)
|
||||||
|
}
|
|
@ -1,8 +1,7 @@
|
||||||
import { instance } from '@/instance';
|
import { instance } from '@/instance';
|
||||||
import { $i } from '@/account';
|
import { $i } from '@/account';
|
||||||
import { api, post } from '@/os';
|
import { api } from '@/os';
|
||||||
import { lang } from '@/config';
|
import { lang } from '@/config';
|
||||||
import { SwMessage } from '@/sw/types';
|
|
||||||
|
|
||||||
export async function initializeSw() {
|
export async function initializeSw() {
|
||||||
if (instance.swPublickey &&
|
if (instance.swPublickey &&
|
||||||
|
@ -50,18 +49,6 @@ export async function initializeSw() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.serviceWorker.addEventListener('message', ev => {
|
|
||||||
const data = ev.data as SwMessage;
|
|
||||||
if (data.type !== 'order') return;
|
|
||||||
|
|
||||||
switch (data.order) {
|
|
||||||
case 'post':
|
|
||||||
return post(data.options);
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert the URL safe base64 string to a Uint8Array
|
* Convert the URL safe base64 string to a Uint8Array
|
||||||
* @param base64String base64 string
|
* @param base64String base64 string
|
||||||
|
|
11
src/client/scripts/login-id.ts
Normal file
11
src/client/scripts/login-id.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export function appendLoginId(url: string, loginId: string) {
|
||||||
|
const u = new URL(url, origin);
|
||||||
|
u.searchParams.append('loginId', loginId);
|
||||||
|
return u.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteLoginId(url: string) {
|
||||||
|
const u = new URL(url);
|
||||||
|
u.searchParams.delete('loginId');
|
||||||
|
return u.toString();
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ declare var self: ServiceWorkerGlobalScope;
|
||||||
import { getNoteSummary } from '../../misc/get-note-summary';
|
import { getNoteSummary } from '../../misc/get-note-summary';
|
||||||
import getUserName from '../../misc/get-user-name';
|
import getUserName from '../../misc/get-user-name';
|
||||||
import { swLang } from '@/sw/lang';
|
import { swLang } from '@/sw/lang';
|
||||||
import { I18n } from '@/scripts/i18n';
|
import { I18n } from '../../misc/i18n';
|
||||||
import { pushNotificationData } from '../../types';
|
import { pushNotificationData } from '../../types';
|
||||||
|
|
||||||
export async function createNotification(data: pushNotificationData) {
|
export async function createNotification(data: pushNotificationData) {
|
||||||
|
@ -18,107 +18,162 @@ async function composeNotification(data: pushNotificationData): Promise<[string,
|
||||||
if (!swLang.i18n) swLang.fetchLocale();
|
if (!swLang.i18n) swLang.fetchLocale();
|
||||||
const i18n = await swLang.i18n as I18n<any>;
|
const i18n = await swLang.i18n as I18n<any>;
|
||||||
const { t } = i18n;
|
const { t } = i18n;
|
||||||
|
const { body } = data;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
/*
|
/*
|
||||||
case 'driveFileCreated': // TODO (Server Side)
|
case 'driveFileCreated': // TODO (Server Side)
|
||||||
return [t('_notification.fileUploaded'), {
|
return [t('_notification.fileUploaded'), {
|
||||||
body: data.body.name,
|
body: body.name,
|
||||||
icon: data.body.url,
|
icon: body.url,
|
||||||
data
|
data
|
||||||
}];
|
}];
|
||||||
*/
|
*/
|
||||||
case 'notification':
|
case 'notification':
|
||||||
switch (data.body.type) {
|
switch (body.type) {
|
||||||
|
case 'follow':
|
||||||
|
return [t('_notification.youWereFollowed'), {
|
||||||
|
body: getUserName(body.user),
|
||||||
|
icon: body.user.avatarUrl,
|
||||||
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'follow',
|
||||||
|
title: t('_notification._actions.followBack')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
case 'mention':
|
case 'mention':
|
||||||
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotMention', { name: getUserName(body.user) }), {
|
||||||
body: getNoteSummary(data.body.note, i18n.locale),
|
body: getNoteSummary(body.note, i18n.locale),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'reply',
|
||||||
|
title: t('_notification._actions.reply')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
case 'reply':
|
||||||
|
return [t('_notification.youGotReply', { name: getUserName(body.user) }), {
|
||||||
|
body: getNoteSummary(body.note, i18n.locale),
|
||||||
|
icon: body.user.avatarUrl,
|
||||||
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'reply',
|
||||||
|
title: t('_notification._actions.reply')
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}];
|
||||||
|
|
||||||
|
case 'renote':
|
||||||
|
return [t('_notification.youRenoted', { name: getUserName(body.user) }), {
|
||||||
|
body: getNoteSummary(body.note.renote, i18n.locale),
|
||||||
|
icon: body.user.avatarUrl,
|
||||||
data,
|
data,
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
action: 'showUser',
|
action: 'showUser',
|
||||||
title: 'showUser'
|
title: getUserName(body.user)
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}];
|
|
||||||
|
|
||||||
case 'reply':
|
|
||||||
return [t('_notification.youGotReply', { name: getUserName(data.body.user) }), {
|
|
||||||
body: getNoteSummary(data.body.note, i18n.locale),
|
|
||||||
icon: data.body.user.avatarUrl,
|
|
||||||
data,
|
|
||||||
}];
|
|
||||||
|
|
||||||
case 'renote':
|
|
||||||
return [t('_notification.youRenoted', { name: getUserName(data.body.user) }), {
|
|
||||||
body: getNoteSummary(data.body.note, i18n.locale),
|
|
||||||
icon: data.body.user.avatarUrl,
|
|
||||||
data,
|
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'quote':
|
case 'quote':
|
||||||
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotQuote', { name: getUserName(body.user) }), {
|
||||||
body: getNoteSummary(data.body.note, i18n.locale),
|
body: getNoteSummary(body.note, i18n.locale),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
data,
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'reply',
|
||||||
|
title: t('_notification._actions.reply')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'renote',
|
||||||
|
title: t('_notification._actions.renote')
|
||||||
|
}
|
||||||
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'reaction':
|
case 'reaction':
|
||||||
return [`${data.body.reaction} ${getUserName(data.body.user)}`, {
|
return [`${body.reaction} ${getUserName(body.user)}`, {
|
||||||
body: getNoteSummary(data.body.note, i18n.locale),
|
body: getNoteSummary(body.note, i18n.locale),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
data,
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'showUser',
|
||||||
|
title: getUserName(body.user)
|
||||||
|
}
|
||||||
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'pollVote':
|
case 'pollVote':
|
||||||
return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotPoll', { name: getUserName(body.user) }), {
|
||||||
body: getNoteSummary(data.body.note, i18n.locale),
|
body: getNoteSummary(body.note, i18n.locale),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
data,
|
|
||||||
}];
|
|
||||||
|
|
||||||
case 'follow':
|
|
||||||
return [t('_notification.youWereFollowed'), {
|
|
||||||
body: getUserName(data.body.user),
|
|
||||||
icon: data.body.user.avatarUrl,
|
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'receiveFollowRequest':
|
case 'receiveFollowRequest':
|
||||||
return [t('_notification.youReceivedFollowRequest'), {
|
return [t('_notification.youReceivedFollowRequest'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
data,
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'accept',
|
||||||
|
title: t('accept')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'reject',
|
||||||
|
title: t('reject')
|
||||||
|
}
|
||||||
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'followRequestAccepted':
|
case 'followRequestAccepted':
|
||||||
return [t('_notification.yourFollowRequestAccepted'), {
|
return [t('_notification.yourFollowRequestAccepted'), {
|
||||||
body: getUserName(data.body.user),
|
body: getUserName(body.user),
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
|
|
||||||
case 'groupInvited':
|
case 'groupInvited':
|
||||||
return [t('_notification.youWereInvitedToGroup'), {
|
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(body.user) }), {
|
||||||
body: data.body.group.name,
|
body: body.invitation.group.name,
|
||||||
data,
|
data,
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'accept',
|
||||||
|
title: t('accept')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
action: 'reject',
|
||||||
|
title: t('reject')
|
||||||
|
}
|
||||||
|
],
|
||||||
}];
|
}];
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
case 'unreadMessagingMessage':
|
case 'unreadMessagingMessage':
|
||||||
if (data.body.groupId === null) {
|
if (body.groupId === null) {
|
||||||
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
|
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(body.user) }), {
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
tag: `messaging:user:${data.body.user.id}`,
|
tag: `messaging:user:${body.userId}`,
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
|
return [t('_notification.youGotMessagingMessageFromGroup', { name: body.group.name }), {
|
||||||
icon: data.body.user.avatarUrl,
|
icon: body.user.avatarUrl,
|
||||||
tag: `messaging:group:${data.body.group.id}`,
|
tag: `messaging:group:${body.groupId}`,
|
||||||
data,
|
data,
|
||||||
}];
|
}];
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -2,12 +2,12 @@ declare var self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
import { get } from 'idb-keyval';
|
import { get } from 'idb-keyval';
|
||||||
import { pushNotificationData } from '../../types';
|
import { pushNotificationData } from '../../types';
|
||||||
|
import { api } from './operations';
|
||||||
|
|
||||||
type Accounts = {
|
type Accounts = {
|
||||||
[x: string]: {
|
[x: string]: {
|
||||||
queue: string[],
|
queue: string[],
|
||||||
timeout: number | null,
|
timeout: number | null
|
||||||
token: string,
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -15,14 +15,13 @@ class SwNotificationRead {
|
||||||
private accounts: Accounts = {};
|
private accounts: Accounts = {};
|
||||||
|
|
||||||
public async construct() {
|
public async construct() {
|
||||||
const accounts = await get('accounts') as { token: string, id: string }[];
|
const accounts = await get('accounts');
|
||||||
if (!accounts) Error('Account is not recorded');
|
if (!accounts) Error('Accounts are not recorded');
|
||||||
|
|
||||||
this.accounts = accounts.reduce((acc, e) => {
|
this.accounts = accounts.reduce((acc, e) => {
|
||||||
acc[e.id] = {
|
acc[e.id] = {
|
||||||
queue: [],
|
queue: [],
|
||||||
timeout: null,
|
timeout: null
|
||||||
token: e.token,
|
|
||||||
};
|
};
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Accounts);
|
}, {} as Accounts);
|
||||||
|
@ -36,21 +35,14 @@ class SwNotificationRead {
|
||||||
|
|
||||||
const account = this.accounts[data.userId];
|
const account = this.accounts[data.userId];
|
||||||
|
|
||||||
account.queue.push(data.body.id);
|
account.queue.push(data.body.id as string);
|
||||||
|
|
||||||
// 最後の呼び出しから200ms待ってまとめて処理する
|
// 最後の呼び出しから200ms待ってまとめて処理する
|
||||||
if (account.timeout) clearTimeout(account.timeout);
|
if (account.timeout) clearTimeout(account.timeout);
|
||||||
account.timeout = setTimeout(() => {
|
account.timeout = setTimeout(() => {
|
||||||
account.timeout = null;
|
account.timeout = null;
|
||||||
|
|
||||||
console.info(account.token, account.queue);
|
api('notifications/read', data.userId, { notificationIds: account.queue });
|
||||||
fetch(`${location.origin}/api/notifications/read`, {
|
|
||||||
method: 'POST',
|
|
||||||
body: JSON.stringify({
|
|
||||||
i: account.token,
|
|
||||||
notificationIds: account.queue
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +0,0 @@
|
||||||
/*
|
|
||||||
* Openers
|
|
||||||
* クライアントを開く関数。
|
|
||||||
* ユーザー、ノート、投稿フォーム、トークルーム
|
|
||||||
*/
|
|
||||||
declare var self: ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
import { SwMessage, swMessageOrderType } from './types';
|
|
||||||
|
|
||||||
// rendered acctからユーザーを開く
|
|
||||||
export async function openUser(acct: string, loginId: string) {
|
|
||||||
open('push-user', { acct }, `${origin}/@${acct}?loginId=${loginId}`, loginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// post-formのオプションから投稿フォームを開く
|
|
||||||
export async function openPost(options: any, loginId: string) {
|
|
||||||
// Build share queries from options
|
|
||||||
let url = `${origin}/?`;
|
|
||||||
if (options.initialText) url += `text=${options.initialText}&`;
|
|
||||||
if (options.reply) url += `replyId=${options.reply.id}&`;
|
|
||||||
if (options.renote) url += `renoteId=${options.renote.id}&`;
|
|
||||||
url += `loginId=${loginId}`;
|
|
||||||
|
|
||||||
open('post', { options }, url, loginId)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function open(order: swMessageOrderType, query: any, url: string, loginId: string) {
|
|
||||||
const client = await self.clients.matchAll({
|
|
||||||
includeUncontrolled: true,
|
|
||||||
type: 'window'
|
|
||||||
}).then(clients => clients.length > 0 ? clients[0] : null);
|
|
||||||
|
|
||||||
if (client) {
|
|
||||||
client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage);
|
|
||||||
|
|
||||||
if ('focus' in client) (client as any).focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return self.clients.openWindow(url);
|
|
||||||
}
|
|
72
src/client/sw/operations.ts
Normal file
72
src/client/sw/operations.ts
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
/*
|
||||||
|
* Operations
|
||||||
|
* 各種操作
|
||||||
|
*/
|
||||||
|
declare var self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
|
import { SwMessage, swMessageOrderType } from './types';
|
||||||
|
import renderAcct from '../../misc/acct/render';
|
||||||
|
import { getAccountFromId } from '@/scripts/get-account-from-id';
|
||||||
|
import { appendLoginId } from '@/scripts/login-id';
|
||||||
|
|
||||||
|
export async function api(endpoint: string, userId: string, options: any = {}) {
|
||||||
|
const account = await getAccountFromId(userId)
|
||||||
|
if (!account) return;
|
||||||
|
|
||||||
|
return fetch(`${origin}/api/${endpoint}`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
i: account.token,
|
||||||
|
...options
|
||||||
|
}),
|
||||||
|
credentials: 'omit',
|
||||||
|
cache: 'no-cache',
|
||||||
|
}).then(async res => {
|
||||||
|
if (!res.ok) Error(`Error while fetching: ${await res.text()}`);
|
||||||
|
|
||||||
|
if (res.status === 200) return res.json();
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// rendered acctからユーザーを開く
|
||||||
|
export function openUser(acct: string, loginId: string) {
|
||||||
|
return openClient('push', `/@${acct}`, loginId, { acct })
|
||||||
|
}
|
||||||
|
|
||||||
|
// noteIdからノートを開く
|
||||||
|
export function openNote(noteId: string, loginId: string) {
|
||||||
|
return openClient('push', `/notes/${noteId}`, loginId, { noteId })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openChat(body: any, loginId: string) {
|
||||||
|
if (body.groupId === null) {
|
||||||
|
return openClient('push', `/my/messaging/${renderAcct(body.user)}`, loginId, { body })
|
||||||
|
} else {
|
||||||
|
return openClient('push', `/my/messaging/group/${body.groupId}`, loginId, { body })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// post-formのオプションから投稿フォームを開く
|
||||||
|
export async function openPost(options: any, loginId: string) {
|
||||||
|
// クエリを作成しておく
|
||||||
|
let url = `/share?`;
|
||||||
|
if (options.initialText) url += `text=${options.initialText}&`;
|
||||||
|
if (options.reply) url += `replyId=${options.reply.id}&`;
|
||||||
|
if (options.renote) url += `renoteId=${options.renote.id}&`;
|
||||||
|
|
||||||
|
return openClient('post', url, loginId, { options })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function openClient(order: swMessageOrderType, url: string, loginId: string, query: any = {}) {
|
||||||
|
const client = await self.clients.matchAll({
|
||||||
|
type: 'window'
|
||||||
|
}).then(clients => clients.length > 0 ? clients[0] as WindowClient : null);
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
client.postMessage({ type: 'order', ...query, order, loginId, url } as SwMessage);
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.clients.openWindow(appendLoginId(url, loginId));
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import { createNotification } from '@/sw/create-notification';
|
||||||
import { swLang } from '@/sw/lang';
|
import { swLang } from '@/sw/lang';
|
||||||
import { swNotificationRead } from '@/sw/notification-read';
|
import { swNotificationRead } from '@/sw/notification-read';
|
||||||
import { pushNotificationData } from '../../types';
|
import { pushNotificationData } from '../../types';
|
||||||
import { openUser } from './open-client';
|
import * as ope from './operations';
|
||||||
import renderAcct from '../../misc/acct/render';
|
import renderAcct from '../../misc/acct/render';
|
||||||
|
|
||||||
//#region Lifecycle: Install
|
//#region Lifecycle: Install
|
||||||
|
@ -55,13 +55,29 @@ self.addEventListener('push', ev => {
|
||||||
return createNotification(data);
|
return createNotification(data);
|
||||||
case 'readAllNotifications':
|
case 'readAllNotifications':
|
||||||
for (const n of await self.registration.getNotifications()) {
|
for (const n of await self.registration.getNotifications()) {
|
||||||
n.close();
|
if (n.data.type === 'notification') n.close();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'readAllMessagingMessages':
|
||||||
|
for (const n of await self.registration.getNotifications()) {
|
||||||
|
if (n.data.type === 'unreadMessagingMessage') n.close();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'readNotifications':
|
case 'readNotifications':
|
||||||
for (const notification of await self.registration.getNotifications()) {
|
for (const n of await self.registration.getNotifications()) {
|
||||||
if (data.body.notificationIds.includes(notification.data.body.id)) {
|
if (data.body.notificationIds?.includes(n.data.body.id)) {
|
||||||
notification.close();
|
n.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'readAllMessagingMessagesOfARoom':
|
||||||
|
for (const n of await self.registration.getNotifications()) {
|
||||||
|
if (n.data.type === 'unreadMessagingMessage'
|
||||||
|
&& ('userId' in data.body
|
||||||
|
? data.body.userId === n.data.body.userId
|
||||||
|
: data.body.groupId === n.data.body.groupId)
|
||||||
|
) {
|
||||||
|
n.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -71,35 +87,93 @@ self.addEventListener('push', ev => {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region Notification
|
//#region Notification
|
||||||
self.addEventListener('notificationclick', async ev => {
|
self.addEventListener('notificationclick', ev => {
|
||||||
|
ev.waitUntil((async () => {
|
||||||
|
|
||||||
|
if (_DEV_) {
|
||||||
|
console.log('notificationclick', ev.action, ev.notification.data);
|
||||||
|
}
|
||||||
|
|
||||||
const { action, notification } = ev;
|
const { action, notification } = ev;
|
||||||
const data: pushNotificationData = notification.data;
|
const data: pushNotificationData = notification.data;
|
||||||
|
const { type, userId: id, body } = data;
|
||||||
|
let client: WindowClient | null = null;
|
||||||
|
let close = true;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
case 'follow':
|
||||||
|
client = await ope.api('following/create', id, { userId: body.userId });
|
||||||
|
break;
|
||||||
case 'showUser':
|
case 'showUser':
|
||||||
switch (data.body.type) {
|
client = await ope.openUser(renderAcct(body.user), id);
|
||||||
case 'reaction':
|
if (body.type !== 'renote') close = false;
|
||||||
return openUser(renderAcct(data.body.user), data.userId);
|
break;
|
||||||
|
case 'reply':
|
||||||
default:
|
client = await ope.openPost({ reply: body.note }, id);
|
||||||
if ('note' in data.body) {
|
break;
|
||||||
return openUser(renderAcct(data.body.data.user), data.userId);
|
case 'renote':
|
||||||
}
|
await ope.api('notes/create', id, { renoteId: body.note.id });
|
||||||
|
break;
|
||||||
|
case 'accept':
|
||||||
|
if (body.type === 'receiveFollowRequest') {
|
||||||
|
await ope.api('following/requests/accept', id, { userId: body.userId });
|
||||||
|
} else if (body.type === 'groupInvited') {
|
||||||
|
await ope.api('users/groups/invitations/accept', id, { invitationId: body.invitation.id });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'reject':
|
||||||
|
if (body.type === 'receiveFollowRequest') {
|
||||||
|
await ope.api('following/requests/reject', id, { userId: body.userId });
|
||||||
|
} else if (body.type === 'groupInvited') {
|
||||||
|
await ope.api('users/groups/invitations/reject', id, { invitationId: body.invitation.id });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'showFollowRequests':
|
||||||
|
client = await ope.openClient('push', '/my/follow-requests', id);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (type === 'unreadMessagingMessage') {
|
||||||
|
client = await ope.openChat(body, id);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// notification.close();
|
switch (body.type) {
|
||||||
|
case 'receiveFollowRequest':
|
||||||
|
client = await ope.openClient('push', '/my/follow-requests', id);
|
||||||
|
break;
|
||||||
|
case 'groupInvited':
|
||||||
|
client = await ope.openClient('push', '/my/groups', id);
|
||||||
|
break;
|
||||||
|
case 'reaction':
|
||||||
|
client = await ope.openNote(body.note.id, id);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if ('note' in body) {
|
||||||
|
client = await ope.openNote(body.note.id, id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if ('user' in body) {
|
||||||
|
client = await ope.openUser(renderAcct(body.data.user), id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (client) {
|
||||||
|
client.focus();
|
||||||
|
}
|
||||||
|
if (type === 'notification') {
|
||||||
|
swNotificationRead.then(that => that.read(data));
|
||||||
|
}
|
||||||
|
if (close) {
|
||||||
|
notification.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
})())
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener('notificationclose', ev => {
|
self.addEventListener('notificationclose', ev => {
|
||||||
const { notification } = ev;
|
const data: pushNotificationData = ev.notification.data;
|
||||||
|
|
||||||
if (!notification.title.startsWith('notification')) {
|
|
||||||
self.registration.showNotification('notificationclose', { body: `${notification?.data?.body?.id}` });
|
|
||||||
}
|
|
||||||
const data: pushNotificationData = notification.data;
|
|
||||||
|
|
||||||
if (data.type === 'notification') {
|
if (data.type === 'notification') {
|
||||||
swNotificationRead.then(that => that.read(data));
|
swNotificationRead.then(that => that.read(data));
|
||||||
|
@ -108,12 +182,15 @@ self.addEventListener('notificationclose', ev => {
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
//#region When: Caught a message from the client
|
//#region When: Caught a message from the client
|
||||||
self.addEventListener('message', ev => {
|
self.addEventListener('message', async ev => {
|
||||||
switch (ev.data) {
|
switch (ev.data) {
|
||||||
case 'clear':
|
case 'clear':
|
||||||
|
// Cache Storage全削除
|
||||||
|
await caches.keys()
|
||||||
|
.then(cacheNames => Promise.all(
|
||||||
|
cacheNames.map(name => caches.delete(name))
|
||||||
|
))
|
||||||
return; // TODO
|
return; // TODO
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof ev.data === 'object') {
|
if (typeof ev.data === 'object') {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export type swMessageOrderType = 'post' | 'push-user' | 'push-note' | 'push-messaging-room';
|
export type swMessageOrderType = 'post' | 'push';
|
||||||
|
|
||||||
export type SwMessage = {
|
export type SwMessage = {
|
||||||
type: 'order';
|
type: 'order';
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream';
|
import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream';
|
||||||
import { publishMessagingStream } from '../../../services/stream';
|
import { publishMessagingStream } from '../../../services/stream';
|
||||||
import { publishMessagingIndexStream } from '../../../services/stream';
|
import { publishMessagingIndexStream } from '../../../services/stream';
|
||||||
|
import { pushNotification } from '../../../services/push-notification';
|
||||||
import { User, ILocalUser, IRemoteUser } from '../../../models/entities/user';
|
import { User, ILocalUser, IRemoteUser } from '../../../models/entities/user';
|
||||||
import { MessagingMessage } from '../../../models/entities/messaging-message';
|
import { MessagingMessage } from '../../../models/entities/messaging-message';
|
||||||
import { MessagingMessages, UserGroupJoinings, Users } from '../../../models';
|
import { MessagingMessages, UserGroupJoinings, Users } from '../../../models';
|
||||||
|
@ -12,6 +13,7 @@ import { renderReadActivity } from '../../../remote/activitypub/renderer/read';
|
||||||
import { renderActivity } from '../../../remote/activitypub/renderer';
|
import { renderActivity } from '../../../remote/activitypub/renderer';
|
||||||
import { deliver } from '../../../queue';
|
import { deliver } from '../../../queue';
|
||||||
import orderedCollection from '../../../remote/activitypub/renderer/ordered-collection';
|
import orderedCollection from '../../../remote/activitypub/renderer/ordered-collection';
|
||||||
|
import { use } from 'matter-js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark messages as read
|
* Mark messages as read
|
||||||
|
@ -50,6 +52,23 @@ export async function readUserMessagingMessage(
|
||||||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||||
publishMainStream(userId, 'readAllMessagingMessages');
|
publishMainStream(userId, 'readAllMessagingMessages');
|
||||||
|
pushNotification(userId, 'readAllMessagingMessages', undefined);
|
||||||
|
} else {
|
||||||
|
// そのユーザーとのメッセージで未読がなければイベント発行
|
||||||
|
const count = await MessagingMessages.count({
|
||||||
|
where: {
|
||||||
|
userId: otherpartyId,
|
||||||
|
recipientId: userId,
|
||||||
|
isRead: false,
|
||||||
|
},
|
||||||
|
take: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!count) {
|
||||||
|
pushNotification(userId, 'readAllMessagingMessagesOfARoom', { userId: otherpartyId });
|
||||||
|
} else {
|
||||||
|
console.log('count')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +123,21 @@ export async function readGroupMessagingMessage(
|
||||||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||||
publishMainStream(userId, 'readAllMessagingMessages');
|
publishMainStream(userId, 'readAllMessagingMessages');
|
||||||
|
pushNotification(userId, 'readAllMessagingMessages', undefined);
|
||||||
|
} else {
|
||||||
|
// そのグループにおいて未読がなければイベント発行
|
||||||
|
const unreadExist = await MessagingMessages.createQueryBuilder('message')
|
||||||
|
.where(`message.groupId = :groupId`, { groupId: groupId })
|
||||||
|
.andWhere('message.userId != :userId', { userId: userId })
|
||||||
|
.andWhere('NOT (:userId = ANY(message.reads))', { userId: userId })
|
||||||
|
.andWhere('message.createdAt > :joinedAt', { joinedAt: joining.createdAt }) // 自分が加入する前の会話については、未読扱いしない
|
||||||
|
.getOne().then(x => x != null)
|
||||||
|
|
||||||
|
if (!unreadExist) {
|
||||||
|
pushNotification(userId, 'readAllMessagingMessagesOfARoom', { groupId });
|
||||||
|
} else {
|
||||||
|
console.log('unread exist')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,6 +11,8 @@ type pushNotificationsTypes = {
|
||||||
'unreadMessagingMessage': PackedMessagingMessage;
|
'unreadMessagingMessage': PackedMessagingMessage;
|
||||||
'readNotifications': { notificationIds: string[] };
|
'readNotifications': { notificationIds: string[] };
|
||||||
'readAllNotifications': undefined;
|
'readAllNotifications': undefined;
|
||||||
|
'readAllMessagingMessages': undefined;
|
||||||
|
'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function pushNotification<T extends keyof pushNotificationsTypes>(userId: string, type: T, body: pushNotificationsTypes[T]) {
|
export async function pushNotification<T extends keyof pushNotificationsTypes>(userId: string, type: T, body: pushNotificationsTypes[T]) {
|
||||||
|
|
17
src/types.ts
17
src/types.ts
|
@ -5,7 +5,18 @@ export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as
|
||||||
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
export const mutedNoteReasons = ['word', 'manual', 'spam', 'other'] as const;
|
||||||
|
|
||||||
export type pushNotificationData = {
|
export type pushNotificationData = {
|
||||||
type: 'notification' | 'unreadMessagingMessage' | 'readNotifications' | 'readAllNotifications',
|
type: 'notification' | 'unreadMessagingMessage' | 'readNotifications' | 'readAllMessagingMessagesOfARoom' | 'readAllNotifications' | 'readAllMessagingMessages';
|
||||||
body: any,
|
body: {
|
||||||
userId: string
|
[x: string]: any;
|
||||||
|
id?: string;
|
||||||
|
type?: typeof notificationTypes[number];
|
||||||
|
notificationIds?: string[];
|
||||||
|
user?: any;
|
||||||
|
userId?: string | null;
|
||||||
|
note?: any;
|
||||||
|
choice?: number;
|
||||||
|
reaction?: string;
|
||||||
|
invitation?: any;
|
||||||
|
};
|
||||||
|
userId: string;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue