✌️
This commit is contained in:
parent
a8c4c74954
commit
bf1db27824
|
@ -1567,7 +1567,7 @@ _notification:
|
|||
youWereFollowed: "フォローされました"
|
||||
youReceivedFollowRequest: "フォローリクエストが来ました"
|
||||
yourFollowRequestAccepted: "フォローリクエストが承認されました"
|
||||
youWereInvitedToGroup: "グループに招待されました"
|
||||
youWereInvitedToGroup: "{userName}があなたをグループに招待しました"
|
||||
|
||||
_types:
|
||||
all: "すべて"
|
||||
|
@ -1583,6 +1583,11 @@ _notification:
|
|||
groupInvited: "グループに招待された"
|
||||
app: "連携アプリからの通知"
|
||||
|
||||
_actions:
|
||||
followBack: "フォローバック"
|
||||
reply: "返信"
|
||||
renote: "Renote"
|
||||
|
||||
_deck:
|
||||
alwaysShowMainColumn: "常にメインカラムを表示"
|
||||
columnAlign: "カラムの寄せ"
|
||||
|
|
|
@ -2,7 +2,7 @@ import { get, set } from 'idb-keyval';
|
|||
import { reactive } from 'vue';
|
||||
import { apiUrl } from '@/config';
|
||||
import { waiting } from '@/os';
|
||||
import { unisonReload } from '@/scripts/unison-reload';
|
||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload';
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
|
@ -89,18 +89,23 @@ export function updateAccount(data) {
|
|||
}
|
||||
|
||||
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();
|
||||
if (_DEV_) console.log('logging as token ', token);
|
||||
const me = await fetchAccount(token);
|
||||
localStorage.setItem('account', JSON.stringify(me));
|
||||
await addAccount(me.id, token);
|
||||
|
||||
if (showTimeline) location.href = '/';
|
||||
else unisonReload();
|
||||
if (href) {
|
||||
reloadChannel.postMessage('reload');
|
||||
location.href = href;
|
||||
return;
|
||||
}
|
||||
|
||||
unisonReload();
|
||||
}
|
||||
|
||||
// このファイルに書きたくないけどここに書かないと何故かVeturが認識しない
|
||||
|
|
|
@ -61,11 +61,14 @@ import * as sound from '@/scripts/sound';
|
|||
import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { makeHotkey } from './scripts/hotkey';
|
||||
import { search } from './scripts/search';
|
||||
import { getThemes } from './theme-store';
|
||||
import { initializeSw } from './scripts/initialize-sw';
|
||||
import { reloadChannel } from './scripts/unison-reload';
|
||||
import { makeHotkey } from '@/scripts/hotkey';
|
||||
import { search } from '@/scripts/search';
|
||||
import { getThemes } from '@/theme-store';
|
||||
import { initializeSw } from '@/scripts/initialize-sw';
|
||||
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}`);
|
||||
|
||||
|
@ -142,6 +145,25 @@ const html = document.documentElement;
|
|||
html.setAttribute('lang', lang);
|
||||
//#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
|
||||
if ($i && $i.token) {
|
||||
if (_DEV_) {
|
||||
|
@ -188,7 +210,7 @@ fetchInstance().then(() => {
|
|||
stream.init($i);
|
||||
|
||||
const app = createApp(await (
|
||||
window.location.search === '?zen' ? import('@/ui/zen.vue') :
|
||||
location.search === '?zen' ? import('@/ui/zen.vue') :
|
||||
!$i ? import('@/ui/visitor.vue') :
|
||||
ui === 'deck' ? import('@/ui/deck.vue') :
|
||||
ui === 'desktop' ? import('@/ui/desktop.vue') :
|
||||
|
@ -217,6 +239,33 @@ components(app);
|
|||
|
||||
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>';
|
||||
|
||||
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 { $i } from '@/account';
|
||||
import { api, post } from '@/os';
|
||||
import { api } from '@/os';
|
||||
import { lang } from '@/config';
|
||||
import { SwMessage } from '@/sw/types';
|
||||
|
||||
export async function initializeSw() {
|
||||
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
|
||||
* @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 getUserName from '../../misc/get-user-name';
|
||||
import { swLang } from '@/sw/lang';
|
||||
import { I18n } from '@/scripts/i18n';
|
||||
import { I18n } from '../../misc/i18n';
|
||||
import { pushNotificationData } from '../../types';
|
||||
|
||||
export async function createNotification(data: pushNotificationData) {
|
||||
|
@ -18,107 +18,162 @@ async function composeNotification(data: pushNotificationData): Promise<[string,
|
|||
if (!swLang.i18n) swLang.fetchLocale();
|
||||
const i18n = await swLang.i18n as I18n<any>;
|
||||
const { t } = i18n;
|
||||
const { body } = data;
|
||||
|
||||
switch (data.type) {
|
||||
/*
|
||||
case 'driveFileCreated': // TODO (Server Side)
|
||||
return [t('_notification.fileUploaded'), {
|
||||
body: data.body.name,
|
||||
icon: data.body.url,
|
||||
body: body.name,
|
||||
icon: body.url,
|
||||
data
|
||||
}];
|
||||
*/
|
||||
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':
|
||||
return [t('_notification.youGotMention', { name: getUserName(data.body.user) }), {
|
||||
body: getNoteSummary(data.body.note, i18n.locale),
|
||||
icon: data.body.user.avatarUrl,
|
||||
return [t('_notification.youGotMention', { name: getUserName(body.user) }), {
|
||||
body: getNoteSummary(body.note, i18n.locale),
|
||||
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,
|
||||
actions: [
|
||||
{
|
||||
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':
|
||||
return [t('_notification.youGotQuote', { name: getUserName(data.body.user) }), {
|
||||
body: getNoteSummary(data.body.note, i18n.locale),
|
||||
icon: data.body.user.avatarUrl,
|
||||
return [t('_notification.youGotQuote', { name: getUserName(body.user) }), {
|
||||
body: getNoteSummary(body.note, i18n.locale),
|
||||
icon: body.user.avatarUrl,
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'reply',
|
||||
title: t('_notification._actions.reply')
|
||||
},
|
||||
{
|
||||
action: 'renote',
|
||||
title: t('_notification._actions.renote')
|
||||
}
|
||||
],
|
||||
}];
|
||||
|
||||
case 'reaction':
|
||||
return [`${data.body.reaction} ${getUserName(data.body.user)}`, {
|
||||
body: getNoteSummary(data.body.note, i18n.locale),
|
||||
icon: data.body.user.avatarUrl,
|
||||
return [`${body.reaction} ${getUserName(body.user)}`, {
|
||||
body: getNoteSummary(body.note, i18n.locale),
|
||||
icon: body.user.avatarUrl,
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'showUser',
|
||||
title: getUserName(body.user)
|
||||
}
|
||||
],
|
||||
}];
|
||||
|
||||
case 'pollVote':
|
||||
return [t('_notification.youGotPoll', { name: getUserName(data.body.user) }), {
|
||||
body: getNoteSummary(data.body.note, i18n.locale),
|
||||
icon: data.body.user.avatarUrl,
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'follow':
|
||||
return [t('_notification.youWereFollowed'), {
|
||||
body: getUserName(data.body.user),
|
||||
icon: data.body.user.avatarUrl,
|
||||
return [t('_notification.youGotPoll', { name: getUserName(body.user) }), {
|
||||
body: getNoteSummary(body.note, i18n.locale),
|
||||
icon: body.user.avatarUrl,
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'receiveFollowRequest':
|
||||
return [t('_notification.youReceivedFollowRequest'), {
|
||||
body: getUserName(data.body.user),
|
||||
icon: data.body.user.avatarUrl,
|
||||
body: getUserName(body.user),
|
||||
icon: body.user.avatarUrl,
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'accept',
|
||||
title: t('accept')
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
title: t('reject')
|
||||
}
|
||||
],
|
||||
}];
|
||||
|
||||
case 'followRequestAccepted':
|
||||
return [t('_notification.yourFollowRequestAccepted'), {
|
||||
body: getUserName(data.body.user),
|
||||
icon: data.body.user.avatarUrl,
|
||||
body: getUserName(body.user),
|
||||
icon: body.user.avatarUrl,
|
||||
data,
|
||||
}];
|
||||
|
||||
case 'groupInvited':
|
||||
return [t('_notification.youWereInvitedToGroup'), {
|
||||
body: data.body.group.name,
|
||||
return [t('_notification.youWereInvitedToGroup', { userName: getUserName(body.user) }), {
|
||||
body: body.invitation.group.name,
|
||||
data,
|
||||
actions: [
|
||||
{
|
||||
action: 'accept',
|
||||
title: t('accept')
|
||||
},
|
||||
{
|
||||
action: 'reject',
|
||||
title: t('reject')
|
||||
}
|
||||
],
|
||||
}];
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
case 'unreadMessagingMessage':
|
||||
if (data.body.groupId === null) {
|
||||
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(data.body.user) }), {
|
||||
icon: data.body.user.avatarUrl,
|
||||
tag: `messaging:user:${data.body.user.id}`,
|
||||
if (body.groupId === null) {
|
||||
return [t('_notification.youGotMessagingMessageFromUser', { name: getUserName(body.user) }), {
|
||||
icon: body.user.avatarUrl,
|
||||
tag: `messaging:user:${body.userId}`,
|
||||
data,
|
||||
}];
|
||||
}
|
||||
return [t('_notification.youGotMessagingMessageFromGroup', { name: data.body.group.name }), {
|
||||
icon: data.body.user.avatarUrl,
|
||||
tag: `messaging:group:${data.body.group.id}`,
|
||||
return [t('_notification.youGotMessagingMessageFromGroup', { name: body.group.name }), {
|
||||
icon: body.user.avatarUrl,
|
||||
tag: `messaging:group:${body.groupId}`,
|
||||
data,
|
||||
}];
|
||||
default:
|
||||
|
|
|
@ -2,12 +2,12 @@ declare var self: ServiceWorkerGlobalScope;
|
|||
|
||||
import { get } from 'idb-keyval';
|
||||
import { pushNotificationData } from '../../types';
|
||||
import { api } from './operations';
|
||||
|
||||
type Accounts = {
|
||||
[x: string]: {
|
||||
queue: string[],
|
||||
timeout: number | null,
|
||||
token: string,
|
||||
timeout: number | null
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -15,14 +15,13 @@ class SwNotificationRead {
|
|||
private accounts: Accounts = {};
|
||||
|
||||
public async construct() {
|
||||
const accounts = await get('accounts') as { token: string, id: string }[];
|
||||
if (!accounts) Error('Account is not recorded');
|
||||
const accounts = await get('accounts');
|
||||
if (!accounts) Error('Accounts are not recorded');
|
||||
|
||||
this.accounts = accounts.reduce((acc, e) => {
|
||||
acc[e.id] = {
|
||||
queue: [],
|
||||
timeout: null,
|
||||
token: e.token,
|
||||
timeout: null
|
||||
};
|
||||
return acc;
|
||||
}, {} as Accounts);
|
||||
|
@ -36,21 +35,14 @@ class SwNotificationRead {
|
|||
|
||||
const account = this.accounts[data.userId];
|
||||
|
||||
account.queue.push(data.body.id);
|
||||
account.queue.push(data.body.id as string);
|
||||
|
||||
// 最後の呼び出しから200ms待ってまとめて処理する
|
||||
if (account.timeout) clearTimeout(account.timeout);
|
||||
account.timeout = setTimeout(() => {
|
||||
account.timeout = null;
|
||||
|
||||
console.info(account.token, account.queue);
|
||||
fetch(`${location.origin}/api/notifications/read`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
i: account.token,
|
||||
notificationIds: account.queue
|
||||
})
|
||||
});
|
||||
api('notifications/read', data.userId, { notificationIds: account.queue });
|
||||
}, 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 { swNotificationRead } from '@/sw/notification-read';
|
||||
import { pushNotificationData } from '../../types';
|
||||
import { openUser } from './open-client';
|
||||
import * as ope from './operations';
|
||||
import renderAcct from '../../misc/acct/render';
|
||||
|
||||
//#region Lifecycle: Install
|
||||
|
@ -55,51 +55,125 @@ self.addEventListener('push', ev => {
|
|||
return createNotification(data);
|
||||
case 'readAllNotifications':
|
||||
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;
|
||||
case 'readNotifications':
|
||||
for (const notification of await self.registration.getNotifications()) {
|
||||
if (data.body.notificationIds.includes(notification.data.body.id)) {
|
||||
notification.close();
|
||||
for (const n of await self.registration.getNotifications()) {
|
||||
if (data.body.notificationIds?.includes(n.data.body.id)) {
|
||||
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;
|
||||
}
|
||||
}));
|
||||
});
|
||||
//#endregion
|
||||
|
||||
//#region Notification
|
||||
self.addEventListener('notificationclick', async ev => {
|
||||
const { action, notification } = ev;
|
||||
const data: pushNotificationData = notification.data;
|
||||
self.addEventListener('notificationclick', ev => {
|
||||
ev.waitUntil((async () => {
|
||||
|
||||
switch (action) {
|
||||
case 'showUser':
|
||||
switch (data.body.type) {
|
||||
case 'reaction':
|
||||
return openUser(renderAcct(data.body.user), data.userId);
|
||||
|
||||
default:
|
||||
if ('note' in data.body) {
|
||||
return openUser(renderAcct(data.body.data.user), data.userId);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (_DEV_) {
|
||||
console.log('notificationclick', ev.action, ev.notification.data);
|
||||
}
|
||||
|
||||
// notification.close();
|
||||
const { action, notification } = ev;
|
||||
const data: pushNotificationData = notification.data;
|
||||
const { type, userId: id, body } = data;
|
||||
let client: WindowClient | null = null;
|
||||
let close = true;
|
||||
|
||||
switch (action) {
|
||||
case 'follow':
|
||||
client = await ope.api('following/create', id, { userId: body.userId });
|
||||
break;
|
||||
case 'showUser':
|
||||
client = await ope.openUser(renderAcct(body.user), id);
|
||||
if (body.type !== 'renote') close = false;
|
||||
break;
|
||||
case 'reply':
|
||||
client = await ope.openPost({ reply: body.note }, id);
|
||||
break;
|
||||
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;
|
||||
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:
|
||||
if (type === 'unreadMessagingMessage') {
|
||||
client = await ope.openChat(body, id);
|
||||
break;
|
||||
}
|
||||
|
||||
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 => {
|
||||
const { notification } = ev;
|
||||
|
||||
if (!notification.title.startsWith('notification')) {
|
||||
self.registration.showNotification('notificationclose', { body: `${notification?.data?.body?.id}` });
|
||||
}
|
||||
const data: pushNotificationData = notification.data;
|
||||
const data: pushNotificationData = ev.notification.data;
|
||||
|
||||
if (data.type === 'notification') {
|
||||
swNotificationRead.then(that => that.read(data));
|
||||
|
@ -108,12 +182,15 @@ self.addEventListener('notificationclose', ev => {
|
|||
//#endregion
|
||||
|
||||
//#region When: Caught a message from the client
|
||||
self.addEventListener('message', ev => {
|
||||
self.addEventListener('message', async ev => {
|
||||
switch (ev.data) {
|
||||
case 'clear':
|
||||
// Cache Storage全削除
|
||||
await caches.keys()
|
||||
.then(cacheNames => Promise.all(
|
||||
cacheNames.map(name => caches.delete(name))
|
||||
))
|
||||
return; // TODO
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
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 = {
|
||||
type: 'order';
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { publishMainStream, publishGroupMessagingStream } from '../../../services/stream';
|
||||
import { publishMessagingStream } from '../../../services/stream';
|
||||
import { publishMessagingIndexStream } from '../../../services/stream';
|
||||
import { pushNotification } from '../../../services/push-notification';
|
||||
import { User, ILocalUser, IRemoteUser } from '../../../models/entities/user';
|
||||
import { MessagingMessage } from '../../../models/entities/messaging-message';
|
||||
import { MessagingMessages, UserGroupJoinings, Users } from '../../../models';
|
||||
|
@ -12,6 +13,7 @@ import { renderReadActivity } from '../../../remote/activitypub/renderer/read';
|
|||
import { renderActivity } from '../../../remote/activitypub/renderer';
|
||||
import { deliver } from '../../../queue';
|
||||
import orderedCollection from '../../../remote/activitypub/renderer/ordered-collection';
|
||||
import { use } from 'matter-js';
|
||||
|
||||
/**
|
||||
* Mark messages as read
|
||||
|
@ -50,6 +52,23 @@ export async function readUserMessagingMessage(
|
|||
if (!await Users.getHasUnreadMessagingMessage(userId)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
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)) {
|
||||
// 全ての(いままで未読だった)自分宛てのメッセージを(これで)読みましたよというイベントを発行
|
||||
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;
|
||||
'readNotifications': { notificationIds: string[] };
|
||||
'readAllNotifications': undefined;
|
||||
'readAllMessagingMessages': undefined;
|
||||
'readAllMessagingMessagesOfARoom': { userId: string } | { groupId: string };
|
||||
};
|
||||
|
||||
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 type pushNotificationData = {
|
||||
type: 'notification' | 'unreadMessagingMessage' | 'readNotifications' | 'readAllNotifications',
|
||||
body: any,
|
||||
userId: string
|
||||
type: 'notification' | 'unreadMessagingMessage' | 'readNotifications' | 'readAllMessagingMessagesOfARoom' | 'readAllNotifications' | 'readAllMessagingMessages';
|
||||
body: {
|
||||
[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