Refactor sw (#10579)
* refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): remove dead code * refactor(sw): 冗長な部分を変更 * refactor(sw): 使われていない煩雑な機能を削除 * refactor(sw): remove dead code * refactor(sw): URL文字列の作成に`URL`を使うように * refactor(sw): 型アサーションの削除とそれに伴い露呈したエラーへの対処 * refactor(sw): `append` -> `set` in `URLSearchParams` * refactor(sw): `any`の削除とそれに伴い露呈したエラーへの対処 * refactor(sw): 型アサーションの削除とそれに伴い露呈したエラーへの対処 対処と言っても`throw`するだけ。いままでもこの状況ではエラーが投げられていたはずなので、この対処により新たな問題が起きることはないはず。 * refactor(sw): i18n loading * refactor(sw): 型推論がうまくできる書き方に変更 `codes`が`(string | undefined)[]`から`string[]`になった * refactor(sw): クエリ文字列の作成に`URLSearchParams`を使うように * refactor(sw): `findClient` * refactor(sw): `openClient`における`any`や`as`の書き換え * refactor(sw): `openPost`における`any`の書き換え * refactor(sw): `let` -> `const` * refactor(sw): `any` -> `unknown` * cleanup(sw): import * cleanup(sw) * cleanup(sw): `?.` * cleanup(sw/.eslintrc.js) * refactor(sw): `@typescript-eslint/explicit-function-return-type` * refactor(sw): `@typescript-eslint/no-unused-vars` * refactor(sw): どうしようもないところに`eslint-disable-next-line`を * refactor(sw): `import/no-default-export` * update operations.ts * throw new Error --------- Co-authored-by: tamaina <tamaina@hotmail.co.jp> Co-authored-by: Kainoa kanter <kainoa@t1c.dev>
This commit is contained in:
parent
479d76d763
commit
599417de6e
13 changed files with 291 additions and 272 deletions
8
packages/sw/src/@types/global.d.ts
vendored
Normal file
8
packages/sw/src/@types/global.d.ts
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type FIXME = any;
|
||||||
|
|
||||||
|
declare const _LANGS_: string[][];
|
||||||
|
declare const _VERSION_: string;
|
||||||
|
declare const _ENV_: string;
|
||||||
|
declare const _DEV_: boolean;
|
||||||
|
declare const _PERF_PREFIX_: string;
|
|
@ -1,14 +0,0 @@
|
||||||
import * as misskey from "calckey-js";
|
|
||||||
import * as Acct from "calckey-js/built/acct";
|
|
||||||
|
|
||||||
export const acct = (user: misskey.Acct) => {
|
|
||||||
return Acct.toString(user);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userName = (user: misskey.entities.User) => {
|
|
||||||
return user.name || user.username;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const userPage = (user: misskey.Acct, path?, absolute = false) => {
|
|
||||||
return `${absolute ? origin : ""}/@${acct(user)}${path ? `/${path}` : ""}`;
|
|
||||||
};
|
|
|
@ -1,23 +1,36 @@
|
||||||
/*
|
/*
|
||||||
* Notification manager for SW
|
* Notification manager for SW
|
||||||
*/
|
*/
|
||||||
declare let self: ServiceWorkerGlobalScope;
|
import type { BadgeNames, PushNotificationDataMap } from "@/types";
|
||||||
|
|
||||||
import { swLang } from "@/scripts/lang";
|
|
||||||
import { cli } from "@/scripts/operations";
|
|
||||||
import { pushNotificationDataMap } from "@/types";
|
|
||||||
import getUserName from "@/scripts/get-user-name";
|
|
||||||
import { I18n } from "@/scripts/i18n";
|
|
||||||
import { getAccountFromId } from "@/scripts/get-account-from-id";
|
|
||||||
import { char2fileName } from "@/scripts/twemoji-base";
|
import { char2fileName } from "@/scripts/twemoji-base";
|
||||||
import * as url from "@/scripts/url";
|
import { cli } from "@/scripts/operations";
|
||||||
|
import { getAccountFromId } from "@/scripts/get-account-from-id";
|
||||||
|
import { swLang } from "@/scripts/lang";
|
||||||
|
import { getUserName } from "@/scripts/get-user-name";
|
||||||
|
|
||||||
const iconUrl = (name: string) =>
|
const closeNotificationsByTags = async (tags: string[]): Promise<void> => {
|
||||||
`/static-assets/notification-badges/${name}.png`;
|
for (const n of (
|
||||||
|
await Promise.all(
|
||||||
|
tags.map((tag) => globalThis.registration.getNotifications({ tag })),
|
||||||
|
)
|
||||||
|
).flat()) {
|
||||||
|
n.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const iconUrl = (name: BadgeNames): string =>
|
||||||
|
`/static-assets/tabler-badges/${name}.png`;
|
||||||
|
/* How to add a new badge:
|
||||||
|
* 1. Find the icon and download png from https://tabler-icons.io/
|
||||||
|
* 2. vips resize ~/Downloads/icon-name.png vipswork.png 0.4; vips scRGB2BW vipswork.png ~/icon-name.png"[compression=9,strip]"; rm vipswork.png;
|
||||||
|
* 3. mv ~/icon-name.png ~/misskey/packages/backend/assets/tabler-badges/
|
||||||
|
* 4. Add 'icon-name' to BadgeNames
|
||||||
|
* 5. Add `badge: iconUrl('icon-name'),`
|
||||||
|
*/
|
||||||
|
|
||||||
export async function createNotification<
|
export async function createNotification<
|
||||||
K extends keyof pushNotificationDataMap,
|
K extends keyof PushNotificationDataMap,
|
||||||
>(data: pushNotificationDataMap[K]) {
|
>(data: PushNotificationDataMap[K]): Promise<void> {
|
||||||
const n = await composeNotification(data);
|
const n = await composeNotification(data);
|
||||||
|
|
||||||
if (n) {
|
if (n) {
|
||||||
|
@ -28,11 +41,10 @@ export async function createNotification<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function composeNotification<K extends keyof pushNotificationDataMap>(
|
async function composeNotification(
|
||||||
data: pushNotificationDataMap[K],
|
data: PushNotificationDataMap[keyof PushNotificationDataMap],
|
||||||
): Promise<[string, NotificationOptions] | null> {
|
): Promise<[string, NotificationOptions] | null> {
|
||||||
if (!swLang.i18n) swLang.fetchLocale();
|
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||||
const i18n = (await swLang.i18n) as I18n<any>;
|
|
||||||
const { t } = i18n;
|
const { t } = i18n;
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
/*
|
/*
|
||||||
|
@ -164,38 +176,20 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(
|
||||||
|
|
||||||
if (reaction.startsWith(":")) {
|
if (reaction.startsWith(":")) {
|
||||||
// カスタム絵文字の場合
|
// カスタム絵文字の場合
|
||||||
const customEmoji = data.body.note.emojis.find(
|
const name = reaction.substring(1, reaction.length - 1);
|
||||||
(x) => x.name === reaction.substr(1, reaction.length - 2),
|
const badgeUrl = new URL(`/emoji/${name}.webp`, origin);
|
||||||
);
|
badgeUrl.searchParams.set("badge", "1");
|
||||||
if (customEmoji) {
|
badge = badgeUrl.href;
|
||||||
if (reaction.includes("@")) {
|
reaction = name.split("@")[0];
|
||||||
reaction = `:${reaction.substr(1, reaction.indexOf("@") - 1)}:`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const u = new URL(customEmoji.url);
|
|
||||||
if (u.href.startsWith(`${origin}/proxy/`)) {
|
|
||||||
// もう既にproxyっぽそうだったらsearchParams付けるだけ
|
|
||||||
u.searchParams.set("badge", "1");
|
|
||||||
badge = u.href;
|
|
||||||
} else {
|
|
||||||
const dummy = `${u.host}${u.pathname}`; // 拡張子がないとキャッシュしてくれないCDNがあるので
|
|
||||||
badge = `${origin}/proxy/${dummy}?${url.query({
|
|
||||||
url: u.href,
|
|
||||||
badge: "1",
|
|
||||||
})}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Unicode絵文字の場合
|
// Unicode絵文字の場合
|
||||||
badge = `/twemoji-badge/${char2fileName(reaction)}.png`;
|
badge = `/twemoji-badge/${char2fileName(reaction)}.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
badge
|
await fetch(badge)
|
||||||
? await fetch(badge)
|
|
||||||
.then((res) => res.status !== 200)
|
.then((res) => res.status !== 200)
|
||||||
.catch(() => true)
|
.catch(() => true)
|
||||||
: true
|
|
||||||
) {
|
) {
|
||||||
badge = iconUrl("plus");
|
badge = iconUrl("plus");
|
||||||
}
|
}
|
||||||
|
@ -339,10 +333,9 @@ async function composeNotification<K extends keyof pushNotificationDataMap>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createEmptyNotification() {
|
export async function createEmptyNotification(): Promise<void> {
|
||||||
return new Promise<void>(async (res) => {
|
return new Promise<void>(async (res) => {
|
||||||
if (!swLang.i18n) swLang.fetchLocale();
|
const i18n = await (swLang.i18n ?? swLang.fetchLocale());
|
||||||
const i18n = (await swLang.i18n) as I18n<any>;
|
|
||||||
const { t } = i18n;
|
const { t } = i18n;
|
||||||
|
|
||||||
await self.registration.showNotification(
|
await self.registration.showNotification(
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { get } from "idb-keyval";
|
import { get } from "idb-keyval";
|
||||||
|
|
||||||
export async function getAccountFromId(id: string) {
|
export async function getAccountFromId(
|
||||||
const accounts = (await get("accounts")) as { token: string; id: string }[];
|
id: string,
|
||||||
if (!accounts) console.log("Accounts are not recorded");
|
): Promise<{ token: string; id: string } | void> {
|
||||||
|
const accounts = await get<{ token: string; id: string }[]>("accounts");
|
||||||
|
if (!accounts) {
|
||||||
|
console.log("Accounts are not recorded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
return accounts.find((e) => e.id === id);
|
return accounts.find((e) => e.id === id);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export default function (user: {
|
export function getUserName(user: {
|
||||||
name?: string | null;
|
name?: string | null;
|
||||||
username: string;
|
username: string;
|
||||||
}): string {
|
}): string {
|
||||||
return user.name || user.username;
|
return user.name === "" ? user.username : user.name ?? user.username;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
export class I18n<T extends Record<string, any>> {
|
export type Locale = { [key: string]: string | Locale };
|
||||||
|
|
||||||
|
export class I18n<T extends Locale = Locale> {
|
||||||
public ts: T;
|
public ts: T;
|
||||||
|
|
||||||
constructor(locale: T) {
|
constructor(locale: T) {
|
||||||
|
@ -15,7 +17,8 @@ export class I18n<T extends Record<string, any>> {
|
||||||
try {
|
try {
|
||||||
let str = key
|
let str = key
|
||||||
.split(".")
|
.split(".")
|
||||||
.reduce((o, i) => o[i], this.ts) as unknown as string;
|
.reduce<Locale | Locale[keyof Locale]>((o, i) => o[i], this.ts);
|
||||||
|
if (typeof str !== "string") throw new Error();
|
||||||
|
|
||||||
if (args) {
|
if (args) {
|
||||||
for (const [k, v] of Object.entries(args)) {
|
for (const [k, v] of Object.entries(args)) {
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Language manager for SW
|
* Language manager for SW
|
||||||
*/
|
*/
|
||||||
declare let self: ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
import { get, set } from "idb-keyval";
|
import { get, set } from "idb-keyval";
|
||||||
import { I18n } from "@/scripts/i18n";
|
import { I18n, type Locale } from "@/scripts/i18n";
|
||||||
|
|
||||||
class SwLang {
|
class SwLang {
|
||||||
public cacheName = `mk-cache-${_VERSION_}`;
|
public cacheName = `mk-cache-${_VERSION_}`;
|
||||||
|
@ -14,19 +12,19 @@ class SwLang {
|
||||||
return prelang;
|
return prelang;
|
||||||
});
|
});
|
||||||
|
|
||||||
public setLang(newLang: string) {
|
public setLang(newLang: string): Promise<I18n<Locale>> {
|
||||||
this.lang = Promise.resolve(newLang);
|
this.lang = Promise.resolve(newLang);
|
||||||
set("lang", newLang);
|
set("lang", newLang);
|
||||||
return this.fetchLocale();
|
return this.fetchLocale();
|
||||||
}
|
}
|
||||||
|
|
||||||
public i18n: Promise<I18n<any>> | null = null;
|
public i18n: Promise<I18n> | null = null;
|
||||||
|
|
||||||
public fetchLocale() {
|
public fetchLocale(): Promise<I18n<Locale>> {
|
||||||
return this.i18n === this._fetch();
|
return (this.i18n = this._fetch());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _fetch() {
|
private async _fetch(): Promise<I18n<Locale>> {
|
||||||
// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
|
// Service Workerは何度も起動しそのたびにlocaleを読み込むので、CacheStorageを使う
|
||||||
const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`;
|
const localeUrl = `/assets/locales/${await this.lang}.${_VERSION_}.json`;
|
||||||
let localeRes = await caches.match(localeUrl);
|
let localeRes = await caches.match(localeUrl);
|
||||||
|
@ -34,13 +32,13 @@ class SwLang {
|
||||||
// _DEV_がtrueの場合は常に最新化
|
// _DEV_がtrueの場合は常に最新化
|
||||||
if (!localeRes || _DEV_) {
|
if (!localeRes || _DEV_) {
|
||||||
localeRes = await fetch(localeUrl);
|
localeRes = await fetch(localeUrl);
|
||||||
const clone = localeRes?.clone();
|
const clone = localeRes.clone();
|
||||||
if (!clone?.clone().ok) Error("locale fetching error");
|
if (!clone.clone().ok) throw new Error("locale fetching error");
|
||||||
|
|
||||||
caches.open(this.cacheName).then((cache) => cache.put(localeUrl, clone));
|
caches.open(this.cacheName).then((cache) => cache.put(localeUrl, clone));
|
||||||
}
|
}
|
||||||
|
|
||||||
return new I18n(await localeRes.json());
|
return new I18n<Locale>(await localeRes.json());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
export function getUrlWithLoginId(url: string, loginId: string) {
|
export function getUrlWithLoginId(url: string, loginId: string): string {
|
||||||
const u = new URL(url, origin);
|
const u = new URL(url, origin);
|
||||||
u.searchParams.append("loginId", loginId);
|
u.searchParams.set("loginId", loginId);
|
||||||
return u.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUrlWithoutLoginId(url: string) {
|
|
||||||
const u = new URL(url);
|
|
||||||
u.searchParams.delete("loginId");
|
|
||||||
return u.toString();
|
return u.toString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,69 +2,100 @@
|
||||||
* Operations
|
* Operations
|
||||||
* 各種操作
|
* 各種操作
|
||||||
*/
|
*/
|
||||||
declare let self: ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
import * as Misskey from "calckey-js";
|
import * as Misskey from "calckey-js";
|
||||||
import { SwMessage, swMessageOrderType } from "@/types";
|
import type { SwMessage, SwMessageOrderType } from "@/types";
|
||||||
import { acct as getAcct } from "@/filters/user";
|
|
||||||
import { getAccountFromId } from "@/scripts/get-account-from-id";
|
import { getAccountFromId } from "@/scripts/get-account-from-id";
|
||||||
import { getUrlWithLoginId } from "@/scripts/login-id";
|
import { getUrlWithLoginId } from "@/scripts/login-id";
|
||||||
|
|
||||||
export const cli = new Misskey.api.APIClient({
|
export const cli = new Misskey.api.APIClient({
|
||||||
origin,
|
origin,
|
||||||
fetch: (...args) => fetch(...args),
|
fetch: (...args): Promise<Response> => fetch(...args),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function api<E extends keyof Misskey.Endpoints>(
|
export async function api<
|
||||||
|
E extends keyof Misskey.Endpoints,
|
||||||
|
O extends Misskey.Endpoints[E]["req"],
|
||||||
|
>(
|
||||||
endpoint: E,
|
endpoint: E,
|
||||||
userId: string,
|
userId?: string,
|
||||||
options?: Misskey.Endpoints[E]["req"],
|
options?: O,
|
||||||
) {
|
): Promise<void | ReturnType<typeof cli.request<E, O>>> {
|
||||||
const account = await getAccountFromId(userId);
|
let account: { token: string; id: string } | void;
|
||||||
if (!account) return;
|
|
||||||
|
|
||||||
return cli.request(endpoint, options, account.token);
|
if (userId) {
|
||||||
|
account = await getAccountFromId(userId);
|
||||||
|
if (!account) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return cli.request(endpoint, options, account?.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark-all-as-read送出を1秒間隔に制限する
|
||||||
|
const readBlockingStatus = new Map<string, boolean>();
|
||||||
|
export function sendMarkAllAsRead(
|
||||||
|
userId: string,
|
||||||
|
): Promise<null | undefined | void> {
|
||||||
|
if (readBlockingStatus.get(userId)) return Promise.resolve();
|
||||||
|
readBlockingStatus.set(userId, true);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
readBlockingStatus.set(userId, false);
|
||||||
|
api("notifications/mark-all-as-read", userId).then(resolve, resolve);
|
||||||
|
}, 1000);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// rendered acctからユーザーを開く
|
// rendered acctからユーザーを開く
|
||||||
export function openUser(acct: string, loginId: string) {
|
export function openUser(
|
||||||
|
acct: string,
|
||||||
|
loginId?: string,
|
||||||
|
): ReturnType<typeof openClient> {
|
||||||
return openClient("push", `/@${acct}`, loginId, { acct });
|
return openClient("push", `/@${acct}`, loginId, { acct });
|
||||||
}
|
}
|
||||||
|
|
||||||
// noteIdからノートを開く
|
// noteIdからノートを開く
|
||||||
export function openNote(noteId: string, loginId: string) {
|
export function openNote(
|
||||||
|
noteId: string,
|
||||||
|
loginId?: string,
|
||||||
|
): ReturnType<typeof openClient> {
|
||||||
return openClient("push", `/notes/${noteId}`, loginId, { noteId });
|
return openClient("push", `/notes/${noteId}`, loginId, { noteId });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openChat(body: any, loginId: string) {
|
// noteIdからノートを開く
|
||||||
if (body.groupId === null) {
|
export function openAntenna(
|
||||||
return openClient("push", `/my/messaging/${getAcct(body.user)}`, loginId, {
|
antennaId: string,
|
||||||
body,
|
loginId: string,
|
||||||
|
): ReturnType<typeof openClient> {
|
||||||
|
return openClient("push", `/timeline/antenna/${antennaId}`, loginId, {
|
||||||
|
antennaId,
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
return openClient("push", `/my/messaging/group/${body.groupId}`, loginId, {
|
|
||||||
body,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// post-formのオプションから投稿フォームを開く
|
// post-formのオプションから投稿フォームを開く
|
||||||
export async function openPost(options: any, loginId: string) {
|
export async function openPost(
|
||||||
|
options: {
|
||||||
|
initialText?: string;
|
||||||
|
reply?: Misskey.entities.Note;
|
||||||
|
renote?: Misskey.entities.Note;
|
||||||
|
},
|
||||||
|
loginId?: string,
|
||||||
|
): ReturnType<typeof openClient> {
|
||||||
// クエリを作成しておく
|
// クエリを作成しておく
|
||||||
let url = "/share?";
|
const url = "/share";
|
||||||
if (options.initialText) url += `text=${options.initialText}&`;
|
const query = new URLSearchParams();
|
||||||
if (options.reply) url += `replyId=${options.reply.id}&`;
|
if (options.initialText) query.set("text", options.initialText);
|
||||||
if (options.renote) url += `renoteId=${options.renote.id}&`;
|
if (options.reply) query.set("replyId", options.reply.id);
|
||||||
|
if (options.renote) query.set("renoteId", options.renote.id);
|
||||||
|
|
||||||
return openClient("post", url, loginId, { options });
|
return openClient("post", `${url}?${query}`, loginId, { options });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openClient(
|
export async function openClient(
|
||||||
order: swMessageOrderType,
|
order: SwMessageOrderType,
|
||||||
url: string,
|
url: string,
|
||||||
loginId: string,
|
loginId?: string,
|
||||||
query: any = {},
|
query: Record<string, SwMessage[string]> = {},
|
||||||
) {
|
): Promise<WindowClient | null> {
|
||||||
const client = await findClient();
|
const client = await findClient();
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
|
@ -74,19 +105,16 @@ export async function openClient(
|
||||||
order,
|
order,
|
||||||
loginId,
|
loginId,
|
||||||
url,
|
url,
|
||||||
} as SwMessage);
|
} satisfies SwMessage);
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.clients.openWindow(getUrlWithLoginId(url, loginId));
|
return self.clients.openWindow(getUrlWithLoginId(url, loginId!));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findClient() {
|
export async function findClient(): Promise<WindowClient | null> {
|
||||||
const clients = await self.clients.matchAll({
|
const clients = await globalThis.clients.matchAll({
|
||||||
type: "window",
|
type: "window",
|
||||||
});
|
});
|
||||||
for (const c of clients) {
|
return clients.find((c) => !new URL(c.url).searchParams.has("zen")) ?? null;
|
||||||
if (c.url.indexOf("?zen") < 0) return c;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
export const twemojiSvgBase = "/twemoji";
|
|
||||||
|
|
||||||
export function char2fileName(char: string): string {
|
export function char2fileName(char: string): string {
|
||||||
let codes = Array.from(char).map((x) => x.codePointAt(0)?.toString(16));
|
let codes = Array.from(char)
|
||||||
|
.map((x) => x.codePointAt(0)?.toString(16))
|
||||||
|
.filter(<T>(x: T | undefined): x is T => x !== undefined);
|
||||||
if (!codes.includes("200d")) codes = codes.filter((x) => x !== "fe0f");
|
if (!codes.includes("200d")) codes = codes.filter((x) => x !== "fe0f");
|
||||||
codes = codes.filter((x) => x?.length);
|
codes = codes.filter((x) => x.length !== 0);
|
||||||
return codes.join("-");
|
return codes.join("-");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function char2filePath(char: string): string {
|
|
||||||
return `${twemojiSvgBase}/${char2fileName(char)}.svg`;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
export function query(obj: {}): string {
|
|
||||||
const params = Object.entries(obj)
|
|
||||||
.filter(([, v]) => (Array.isArray(v) ? v.length : v !== undefined))
|
|
||||||
.reduce((a, [k, v]) => ((a[k] = v), a), {} as Record<string, any>);
|
|
||||||
|
|
||||||
return Object.entries(params)
|
|
||||||
.map((e) => `${e[0]}=${encodeURIComponent(e[1])}`)
|
|
||||||
.join("&");
|
|
||||||
}
|
|
||||||
|
|
||||||
export function appendQuery(url: string, query: string): string {
|
|
||||||
return `${url}${
|
|
||||||
/\?/.test(url) ? (url.endsWith("?") ? "" : "&") : "?"
|
|
||||||
}${query}`;
|
|
||||||
}
|
|
|
@ -1,20 +1,18 @@
|
||||||
declare let self: ServiceWorkerGlobalScope;
|
import { get } from "idb-keyval";
|
||||||
|
import * as Acct from "calckey-js/built/acct";
|
||||||
|
import type { PushNotificationDataMap } from "@/types";
|
||||||
import {
|
import {
|
||||||
createEmptyNotification,
|
createEmptyNotification,
|
||||||
createNotification,
|
createNotification,
|
||||||
} from "@/scripts/create-notification";
|
} from "@/scripts/create-notification";
|
||||||
import { swLang } from "@/scripts/lang";
|
import { swLang } from "@/scripts/lang";
|
||||||
import { swNotificationRead } from "@/scripts/notification-read";
|
|
||||||
import { pushNotificationDataMap } from "@/types";
|
|
||||||
import * as swos from "@/scripts/operations";
|
import * as swos from "@/scripts/operations";
|
||||||
import { acct as getAcct } from "@/filters/user";
|
|
||||||
|
|
||||||
self.addEventListener("install", (ev) => {
|
globalThis.addEventListener("install", () => {
|
||||||
ev.waitUntil(self.skipWaiting());
|
// ev.waitUntil(globalThis.skipWaiting());
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("activate", (ev) => {
|
globalThis.addEventListener("activate", (ev) => {
|
||||||
ev.waitUntil(
|
ev.waitUntil(
|
||||||
caches
|
caches
|
||||||
.keys()
|
.keys()
|
||||||
|
@ -25,11 +23,15 @@ self.addEventListener("activate", (ev) => {
|
||||||
.map((name) => caches.delete(name)),
|
.map((name) => caches.delete(name)),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.then(() => self.clients.claim()),
|
.then(() => globalThis.clients.claim()),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("fetch", (ev) => {
|
function offlineContentHTML(): string {
|
||||||
|
return `<!doctype html>Offline. Service Worker @${_VERSION_} <button onclick="location.reload()">reload</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.addEventListener("fetch", (ev) => {
|
||||||
let isHTMLRequest = false;
|
let isHTMLRequest = false;
|
||||||
if (ev.request.headers.get("sec-fetch-dest") === "document") {
|
if (ev.request.headers.get("sec-fetch-dest") === "document") {
|
||||||
isHTMLRequest = true;
|
isHTMLRequest = true;
|
||||||
|
@ -41,90 +43,68 @@ self.addEventListener("fetch", (ev) => {
|
||||||
|
|
||||||
if (!isHTMLRequest) return;
|
if (!isHTMLRequest) return;
|
||||||
ev.respondWith(
|
ev.respondWith(
|
||||||
fetch(ev.request).catch(
|
fetch(ev.request).catch(() => {
|
||||||
() =>
|
return new Response(offlineContentHTML(), {
|
||||||
new Response(`Offline. Service Worker @${_VERSION_}`, { status: 200 }),
|
status: 200,
|
||||||
),
|
headers: {
|
||||||
|
"content-type": "text/html",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener("push", (ev) => {
|
globalThis.addEventListener("push", (ev) => {
|
||||||
// クライアント取得
|
// クライアント取得
|
||||||
ev.waitUntil(
|
ev.waitUntil(
|
||||||
self.clients
|
globalThis.clients
|
||||||
.matchAll({
|
.matchAll({
|
||||||
includeUncontrolled: true,
|
includeUncontrolled: true,
|
||||||
type: "window",
|
type: "window",
|
||||||
})
|
})
|
||||||
.then(
|
.then(async () => {
|
||||||
async <K extends keyof pushNotificationDataMap>(
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] =
|
||||||
clients: readonly WindowClient[],
|
ev.data?.json();
|
||||||
) => {
|
|
||||||
const data: pushNotificationDataMap[K] = ev.data?.json();
|
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
// case 'driveFileCreated':
|
// case 'driveFileCreated':
|
||||||
case "notification":
|
case "notification":
|
||||||
case "unreadMessagingMessage":
|
case "unreadAntennaNote":
|
||||||
// 1日以上経過している場合は無視
|
// 1日以上経過している場合は無視
|
||||||
if (new Date().getTime() - data.dateTime > 1000 * 60 * 60 * 24)
|
if (new Date().getTime() - data.dateTime > 1000 * 60 * 60 * 24)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// クライアントがあったらストリームに接続しているということなので通知しない
|
|
||||||
if (clients.length !== 0) break;
|
|
||||||
|
|
||||||
return createNotification(data);
|
return createNotification(data);
|
||||||
case "readAllNotifications":
|
case "readAllNotifications":
|
||||||
for (const n of await self.registration.getNotifications()) {
|
await globalThis.registration
|
||||||
if (n?.data?.type === "notification") n.close();
|
.getNotifications()
|
||||||
}
|
.then((notifications) =>
|
||||||
break;
|
notifications.forEach(
|
||||||
case "readAllMessagingMessages":
|
(n) => n.tag !== "read_notification" && n.close(),
|
||||||
for (const n of await self.registration.getNotifications()) {
|
),
|
||||||
if (n?.data?.type === "unreadMessagingMessage") n.close();
|
);
|
||||||
}
|
|
||||||
break;
|
|
||||||
case "readNotifications":
|
|
||||||
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;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return createEmptyNotification();
|
await createEmptyNotification();
|
||||||
},
|
return;
|
||||||
),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
self.addEventListener(
|
(globalThis as unknown as ServiceWorkerGlobalScope).addEventListener(
|
||||||
"notificationclick",
|
"notificationclick",
|
||||||
<K extends keyof pushNotificationDataMap>(
|
(ev: ServiceWorkerGlobalScopeEventMap["notificationclick"]) => {
|
||||||
ev: ServiceWorkerGlobalScopeEventMap["notificationclick"],
|
|
||||||
) => {
|
|
||||||
ev.waitUntil(
|
ev.waitUntil(
|
||||||
(async () => {
|
(async (): Promise<void> => {
|
||||||
if (_DEV_) {
|
if (_DEV_) {
|
||||||
console.log("notificationclick", ev.action, ev.notification.data);
|
console.log("notificationclick", ev.action, ev.notification.data);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { action, notification } = ev;
|
const { action, notification } = ev;
|
||||||
const data: pushNotificationDataMap[K] = notification.data;
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] =
|
||||||
const { userId: id } = data;
|
notification.data ?? {};
|
||||||
|
const { userId: loginId } = data;
|
||||||
let client: WindowClient | null = null;
|
let client: WindowClient | null = null;
|
||||||
|
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
@ -132,57 +112,53 @@ self.addEventListener(
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "follow":
|
case "follow":
|
||||||
if ("userId" in data.body)
|
if ("userId" in data.body)
|
||||||
await swos.api("following/create", id, {
|
await swos.api("following/create", loginId, {
|
||||||
userId: data.body.userId,
|
userId: data.body.userId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "showUser":
|
case "showUser":
|
||||||
if ("user" in data.body)
|
if ("user" in data.body)
|
||||||
client = await swos.openUser(getAcct(data.body.user), id);
|
client = await swos.openUser(
|
||||||
|
Acct.toString(data.body.user),
|
||||||
|
loginId,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "reply":
|
case "reply":
|
||||||
if ("note" in data.body)
|
if ("note" in data.body)
|
||||||
client = await swos.openPost({ reply: data.body.note }, id);
|
client = await swos.openPost(
|
||||||
|
{ reply: data.body.note },
|
||||||
|
loginId,
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case "renote":
|
case "renote":
|
||||||
if ("note" in data.body)
|
if ("note" in data.body)
|
||||||
await swos.api("notes/create", id, {
|
await swos.api("notes/create", loginId, {
|
||||||
renoteId: data.body.note.id,
|
renoteId: data.body.note.id,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "accept":
|
case "accept":
|
||||||
switch (data.body.type) {
|
switch (data.body.type) {
|
||||||
case "receiveFollowRequest":
|
case "receiveFollowRequest":
|
||||||
await swos.api("following/requests/accept", id, {
|
await swos.api("following/requests/accept", loginId, {
|
||||||
userId: data.body.userId,
|
userId: data.body.userId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "groupInvited":
|
|
||||||
await swos.api("users/groups/invitations/accept", id, {
|
|
||||||
invitationId: data.body.invitation.id,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "reject":
|
case "reject":
|
||||||
switch (data.body.type) {
|
switch (data.body.type) {
|
||||||
case "receiveFollowRequest":
|
case "receiveFollowRequest":
|
||||||
await swos.api("following/requests/reject", id, {
|
await swos.api("following/requests/reject", loginId, {
|
||||||
userId: data.body.userId,
|
userId: data.body.userId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "groupInvited":
|
|
||||||
await swos.api("users/groups/invitations/reject", id, {
|
|
||||||
invitationId: data.body.invitation.id,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "showFollowRequests":
|
case "showFollowRequests":
|
||||||
client = await swos.openClient(
|
client = await swos.openClient(
|
||||||
"push",
|
"push",
|
||||||
"/my/follow-requests",
|
"/my/follow-requests",
|
||||||
id,
|
loginId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -191,35 +167,61 @@ self.addEventListener(
|
||||||
client = await swos.openClient(
|
client = await swos.openClient(
|
||||||
"push",
|
"push",
|
||||||
"/my/follow-requests",
|
"/my/follow-requests",
|
||||||
id,
|
loginId,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case "groupInvited":
|
|
||||||
client = await swos.openClient("push", "/my/groups", id);
|
|
||||||
break;
|
|
||||||
case "reaction":
|
case "reaction":
|
||||||
client = await swos.openNote(data.body.note.id, id);
|
client = await swos.openNote(data.body.note.id, loginId);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
if ("note" in data.body) {
|
if ("note" in data.body) {
|
||||||
client = await swos.openNote(data.body.note.id, id);
|
client = await swos.openNote(data.body.note.id, loginId);
|
||||||
} else if ("user" in data.body) {
|
} else if ("user" in data.body) {
|
||||||
client = await swos.openUser(getAcct(data.body.user), id);
|
client = await swos.openUser(
|
||||||
|
Acct.toString(data.body.user),
|
||||||
|
loginId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "unreadMessagingMessage":
|
case "unreadAntennaNote":
|
||||||
client = await swos.openChat(data.body, id);
|
client = await swos.openAntenna(data.body.antenna.id, loginId);
|
||||||
break;
|
break;
|
||||||
|
default:
|
||||||
|
switch (action) {
|
||||||
|
case "markAllAsRead":
|
||||||
|
await globalThis.registration
|
||||||
|
.getNotifications()
|
||||||
|
.then((notifications) =>
|
||||||
|
notifications.forEach(
|
||||||
|
(n) => n.tag !== "read_notification" && n.close(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await get("accounts").then((accounts) => {
|
||||||
|
return Promise.all(
|
||||||
|
accounts.map(async (account) => {
|
||||||
|
await swos.sendMarkAllAsRead(account.id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "settings":
|
||||||
|
client = await swos.openClient(
|
||||||
|
"push",
|
||||||
|
"/settings/notifications",
|
||||||
|
loginId,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client) {
|
if (client) {
|
||||||
client.focus();
|
client.focus();
|
||||||
}
|
}
|
||||||
if (data.type === "notification") {
|
if (data.type === "notification") {
|
||||||
swNotificationRead.then((that) => that.read(data));
|
await swos.sendMarkAllAsRead(loginId);
|
||||||
}
|
}
|
||||||
|
|
||||||
notification.close();
|
notification.close();
|
||||||
|
@ -228,24 +230,28 @@ self.addEventListener(
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
self.addEventListener(
|
(globalThis as unknown as ServiceWorkerGlobalScope).addEventListener(
|
||||||
"notificationclose",
|
"notificationclose",
|
||||||
<K extends keyof pushNotificationDataMap>(
|
(ev: ServiceWorkerGlobalScopeEventMap["notificationclose"]) => {
|
||||||
ev: ServiceWorkerGlobalScopeEventMap["notificationclose"],
|
const data: PushNotificationDataMap[keyof PushNotificationDataMap] =
|
||||||
) => {
|
ev.notification.data;
|
||||||
const data: pushNotificationDataMap[K] = ev.notification.data;
|
|
||||||
|
|
||||||
|
ev.waitUntil(
|
||||||
|
(async (): Promise<void> => {
|
||||||
if (data.type === "notification") {
|
if (data.type === "notification") {
|
||||||
swNotificationRead.then((that) => that.read(data));
|
await swos.sendMarkAllAsRead(data.userId);
|
||||||
}
|
}
|
||||||
|
return;
|
||||||
|
})(),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
self.addEventListener(
|
(globalThis as unknown as ServiceWorkerGlobalScope).addEventListener(
|
||||||
"message",
|
"message",
|
||||||
(ev: ServiceWorkerGlobalScopeEventMap["message"]) => {
|
(ev: ServiceWorkerGlobalScopeEventMap["message"]) => {
|
||||||
ev.waitUntil(
|
ev.waitUntil(
|
||||||
(async () => {
|
(async (): Promise<void> => {
|
||||||
switch (ev.data) {
|
switch (ev.data) {
|
||||||
case "clear":
|
case "clear":
|
||||||
// Cache Storage全削除
|
// Cache Storage全削除
|
||||||
|
|
|
@ -1,34 +1,51 @@
|
||||||
import * as Misskey from "calckey-js";
|
import * as Misskey from "calckey-js";
|
||||||
|
|
||||||
export type swMessageOrderType = "post" | "push";
|
export type SwMessageOrderType = "post" | "push";
|
||||||
|
|
||||||
export type SwMessage = {
|
export type SwMessage = {
|
||||||
type: "order";
|
type: "order";
|
||||||
order: swMessageOrderType;
|
order: SwMessageOrderType;
|
||||||
loginId: string;
|
loginId?: string;
|
||||||
url: string;
|
url: string;
|
||||||
[x: string]: any;
|
[x: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Defined also @/services/push-notification.ts#L7-L14
|
// Defined also @/services/push-notification.ts#L7-L14
|
||||||
type pushNotificationDataSourceMap = {
|
type PushNotificationDataSourceMap = {
|
||||||
notification: Misskey.entities.Notification;
|
notification: Misskey.entities.Notification;
|
||||||
unreadMessagingMessage: Misskey.entities.MessagingMessage;
|
unreadAntennaNote: {
|
||||||
readNotifications: { notificationIds: string[] };
|
antenna: { id: string; name: string };
|
||||||
|
note: Misskey.entities.Note;
|
||||||
|
};
|
||||||
readAllNotifications: undefined;
|
readAllNotifications: undefined;
|
||||||
readAllMessagingMessages: undefined;
|
readAllMessagingMessages: undefined;
|
||||||
readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
|
readAllMessagingMessagesOfARoom: { userId: string } | { groupId: string };
|
||||||
};
|
};
|
||||||
|
|
||||||
export type pushNotificationData<
|
export type PushNotificationData<
|
||||||
K extends keyof pushNotificationDataSourceMap,
|
K extends keyof PushNotificationDataSourceMap,
|
||||||
> = {
|
> = {
|
||||||
type: K;
|
type: K;
|
||||||
body: pushNotificationDataSourceMap[K];
|
body: PushNotificationDataSourceMap[K];
|
||||||
userId: string;
|
userId: string;
|
||||||
dateTime: number;
|
dateTime: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type pushNotificationDataMap = {
|
export type PushNotificationDataMap = {
|
||||||
[K in keyof pushNotificationDataSourceMap]: pushNotificationData<K>;
|
[K in keyof PushNotificationDataSourceMap]: PushNotificationData<K>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type BadgeNames =
|
||||||
|
| "null"
|
||||||
|
| "antenna"
|
||||||
|
| "arrow-back-up"
|
||||||
|
| "at"
|
||||||
|
| "chart-arrows"
|
||||||
|
| "circle-check"
|
||||||
|
| "medal"
|
||||||
|
| "messages"
|
||||||
|
| "plus"
|
||||||
|
| "quote"
|
||||||
|
| "repeat"
|
||||||
|
| "user-plus"
|
||||||
|
| "users";
|
||||||
|
|
Loading…
Reference in a new issue