Merge branch 'develop' into feature/capacitor

This commit is contained in:
ThatOneCalculator 2023-04-04 21:27:07 -07:00
commit 1f9e35f0cf
No known key found for this signature in database
GPG Key ID: 8703CACD01000000
59 changed files with 726 additions and 175 deletions

View File

@ -18,4 +18,4 @@ services:
image: redis image: redis
branches: branches:
include: [ main, develop, feature/* ] include: [ main, beta, develop, feature/* ]

View File

@ -3,7 +3,7 @@ FROM node:19-alpine as build
WORKDIR /calckey WORKDIR /calckey
# Install compilation dependencies # Install compilation dependencies
RUN apk add --no-cache --no-progress git alpine-sdk python3 rust cargo RUN apk add --no-cache --no-progress git alpine-sdk python3 rust cargo vips
# Copy only the dependency-related files first, to cache efficiently # Copy only the dependency-related files first, to cache efficiently
COPY package.json pnpm*.yaml ./ COPY package.json pnpm*.yaml ./
@ -11,7 +11,8 @@ COPY packages/backend/package.json packages/backend/package.json
COPY packages/client/package.json packages/client/package.json COPY packages/client/package.json packages/client/package.json
COPY packages/sw/package.json packages/sw/package.json COPY packages/sw/package.json packages/sw/package.json
COPY packages/backend/native-utils/package.json packages/backend/native-utils/package.json COPY packages/backend/native-utils/package.json packages/backend/native-utils/package.json
COPY packages/backend/native-utils/**/*/package.json packages/backend/native-utils/**/*/package.json COPY packages/backend/native-utils/npm/linux-x64-musl/package.json packages/backend/native-utils/npm/linux-x64-musl/package.json
COPY packages/backend/native-utils/npm/linux-arm64-musl/package.json packages/backend/native-utils/npm/linux-arm64-musl/package.json
# Configure corepack and pnpm # Configure corepack and pnpm
RUN corepack enable RUN corepack enable
@ -47,6 +48,8 @@ COPY --from=build /calckey/packages/client/node_modules /calckey/packages/client
COPY --from=build /calckey/built /calckey/built COPY --from=build /calckey/built /calckey/built
COPY --from=build /calckey/packages/backend/built /calckey/packages/backend/built COPY --from=build /calckey/packages/backend/built /calckey/packages/backend/built
COPY --from=build /calckey/packages/backend/assets/instance.css /calckey/packages/backend/assets/instance.css COPY --from=build /calckey/packages/backend/assets/instance.css /calckey/packages/backend/assets/instance.css
COPY --from=build /calckey/packages/backend/native-utils/built /calckey/packages/backend/native-utils/built
COPY --from=build /calckey/packages/backend/native-utils/target /calckey/packages/backend/native-utils/target
RUN corepack enable RUN corepack enable
ENTRYPOINT [ "/sbin/tini", "--" ] ENTRYPOINT [ "/sbin/tini", "--" ]

View File

@ -12,6 +12,7 @@ fetchingAsApObject: "Fetching from the Fediverse"
ok: "OK" ok: "OK"
gotIt: "Got it!" gotIt: "Got it!"
cancel: "Cancel" cancel: "Cancel"
noThankYou: "No thank you"
enterUsername: "Enter username" enterUsername: "Enter username"
renotedBy: "Boosted by {user}" renotedBy: "Boosted by {user}"
noNotes: "No posts" noNotes: "No posts"
@ -146,6 +147,8 @@ flagAsBot: "Mark this account as a bot"
flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Calckey's internal systems to treat this account as a bot." flagAsBotDescription: "Enable this option if this account is controlled by a program. If enabled, it will act as a flag for other developers to prevent endless interaction chains with other bots and adjust Calckey's internal systems to treat this account as a bot."
flagAsCat: "Are you a cat? 😺" flagAsCat: "Are you a cat? 😺"
flagAsCatDescription: "You'll get cat ears and speak like a cat!" flagAsCatDescription: "You'll get cat ears and speak like a cat!"
flagSpeakAsCat: "Speak as a cat"
flagSpeakAsCatDescription: "Your posts will get nyanified when in cat mode"
flagShowTimelineReplies: "Show replies in timeline" flagShowTimelineReplies: "Show replies in timeline"
flagShowTimelineRepliesDescription: "Shows replies of users to posts of other users in the timeline if turned on." flagShowTimelineRepliesDescription: "Shows replies of users to posts of other users in the timeline if turned on."
autoAcceptFollowed: "Automatically approve follow requests from users you're following" autoAcceptFollowed: "Automatically approve follow requests from users you're following"
@ -914,6 +917,13 @@ navbar: "Navigation bar"
shuffle: "Shuffle" shuffle: "Shuffle"
account: "Account" account: "Account"
move: "Move" move: "Move"
pushNotification: "Push notifications"
subscribePushNotification: "Enable push notifications"
unsubscribePushNotification: "Disable push notifications"
pushNotificationAlreadySubscribed: "Push notifications are already enabled"
pushNotificationNotSupported: "Your browser or instance does not support push notifications"
sendPushNotificationReadMessage: "Delete push notifications once the relevant notifications or messages have been read"
sendPushNotificationReadMessageCaption: "A notification containing the text \"{emptyPushNotificationMessage}\" will be displayed for a short time. This may increase the battery usage of your device, if applicable."
showAds: "Show ads" showAds: "Show ads"
enterSendsMessage: "Press Return in Messaging to send message (off is Ctrl + Return)" enterSendsMessage: "Press Return in Messaging to send message (off is Ctrl + Return)"
adminCustomCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause EVERYONE'S clients to stop functioning normally. Please ensure your CSS works properly by testing it in your user settings." adminCustomCssWarn: "This setting should only be used if you know what it does. Entering improper values may cause EVERYONE'S clients to stop functioning normally. Please ensure your CSS works properly by testing it in your user settings."
@ -1228,13 +1238,13 @@ _sfx:
_ago: _ago:
future: "Future" future: "Future"
justNow: "Just now" justNow: "Just now"
secondsAgo: "{n} second(s) ago" secondsAgo: "{n}s ago"
minutesAgo: "{n} minute(s) ago" minutesAgo: "{n}m ago"
hoursAgo: "{n} hour(s) ago" hoursAgo: "{n}h ago"
daysAgo: "{n} day(s) ago" daysAgo: "{n}d ago"
weeksAgo: "{n} week(s) ago" weeksAgo: "{n}w ago"
monthsAgo: "{n} month(s) ago" monthsAgo: "{n}mo ago"
yearsAgo: "{n} year(s) ago" yearsAgo: "{n}y ago"
_time: _time:
second: "Second(s)" second: "Second(s)"
minute: "Minute(s)" minute: "Minute(s)"

View File

@ -12,6 +12,7 @@ fetchingAsApObject: "連合宇宙から取得中"
ok: "OK" ok: "OK"
gotIt: "わかった!" gotIt: "わかった!"
cancel: "キャンセル" cancel: "キャンセル"
noThankYou: "やめておく"
enterUsername: "ユーザー名を入力" enterUsername: "ユーザー名を入力"
renotedBy: "{user}がブースト" renotedBy: "{user}がブースト"
noNotes: "投稿はありません" noNotes: "投稿はありません"
@ -145,7 +146,9 @@ cacheRemoteFilesDescription: "この設定を無効にすると、リモート
flagAsBot: "Botとして設定" flagAsBot: "Botとして設定"
flagAsBotDescription: "このアカウントがBotである場合は、この設定をオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Calckeyのシステム上での扱いがBotに合ったものになります。" flagAsBotDescription: "このアカウントがBotである場合は、この設定をオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Calckeyのシステム上での扱いがBotに合ったものになります。"
flagAsCat: "あなたは…猫?😺" flagAsCat: "あなたは…猫?😺"
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。" flagAsCatDescription: "このアカウントが猫であることを示す猫モードを有効にするには、このフラグをオンにします。"
flagSpeakAsCat: "猫語で話す"
flagSpeakAsCatDescription: "猫モードが有効の場合にオンにすると、あなたの投稿の「な」を「にゃ」に変換します。"
flagShowTimelineReplies: "タイムラインに投稿の返信を表示する" flagShowTimelineReplies: "タイムラインに投稿の返信を表示する"
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーの他の投稿への返信も表示されます。" flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーの他の投稿への返信も表示されます。"
autoAcceptFollowed: "フォローしているユーザーからのフォロー申請を自動承認" autoAcceptFollowed: "フォローしているユーザーからのフォロー申請を自動承認"
@ -916,6 +919,13 @@ navbar: "ナビゲーションバー"
shuffle: "シャッフル" shuffle: "シャッフル"
account: "アカウント" account: "アカウント"
move: "移動" move: "移動"
pushNotification: "プッシュ通知"
subscribePushNotification: "プッシュ通知を有効化"
unsubscribePushNotification: "プッシュ通知を停止する"
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
adminCustomCssWarn: "この設定は、それが何をするものであるかを知っている場合のみ使用してください。不適切な値を入力すると、クライアントが正常に動作しなくなる可能性があります。ユーザー設定でCSSをテストし、正しく動作することを確認してください。" adminCustomCssWarn: "この設定は、それが何をするものであるかを知っている場合のみ使用してください。不適切な値を入力すると、クライアントが正常に動作しなくなる可能性があります。ユーザー設定でCSSをテストし、正しく動作することを確認してください。"
customMOTD: "カスタムMOTD(スプラッシュスクリーンメッセージ)" customMOTD: "カスタムMOTD(スプラッシュスクリーンメッセージ)"
customMOTDDescription: "ユーザがページをロード/リロードするたびにランダムに表示される、改行で区切られたMOTD(スプラッシュスクリーン)用のカスタムメッセージ" customMOTDDescription: "ユーザがページをロード/リロードするたびにランダムに表示される、改行で区切られたMOTD(スプラッシュスクリーン)用のカスタムメッセージ"

View File

@ -1,12 +1,12 @@
{ {
"name": "calckey", "name": "calckey",
"version": "13.2.0-dev23", "version": "13.2.0-dev26",
"codename": "aqua", "codename": "aqua",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://codeberg.org/calckey/calckey.git" "url": "https://codeberg.org/calckey/calckey.git"
}, },
"packageManager": "pnpm@8.1.0", "packageManager": "pnpm@8.1.1",
"private": true, "private": true,
"scripts": { "scripts": {
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp", "rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",

View File

@ -0,0 +1,11 @@
export class whetherPushNotifyToSendReadMessage1669138716634 {
name = 'whetherPushNotifyToSendReadMessage1669138716634'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "sw_subscription" ADD "sendReadMessage" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "sw_subscription" DROP COLUMN "sendReadMessage"`);
}
}

View File

@ -0,0 +1,20 @@
export class SpeakAsCat1680426269172 {
name = 'SpeakAsCat1680426269172'
async up(queryRunner) {
await queryRunner.query(`
ALTER TABLE "user"
ADD "speakAsCat" boolean NOT NULL DEFAULT true
`);
await queryRunner.query(`
COMMENT ON COLUMN "user"."speakAsCat"
IS 'Whether to speak as a cat if isCat.'
`);
}
async down(queryRunner) {
await queryRunner.query(`
ALTER TABLE "user" DROP COLUMN "speakAsCat"
`);
}
}

View File

@ -41,4 +41,9 @@ export class SwSubscription {
length: 128, length: 128,
}) })
public publickey: string; public publickey: string;
@Column('boolean', {
default: false,
})
public sendReadMessage: boolean;
} }

View File

@ -156,6 +156,12 @@ export class User {
}) })
public isCat: boolean; public isCat: boolean;
@Column('boolean', {
default: true,
comment: 'Whether to speak as a cat if isCat.',
})
public speakAsCat: boolean;
@Column('boolean', { @Column('boolean', {
default: false, default: false,
comment: 'Whether the User is the admin.', comment: 'Whether the User is the admin.',

View File

@ -263,7 +263,7 @@ export const NoteRepository = db.getRepository(Note).extend({
: {}), : {}),
}); });
if (packed.user.isCat && packed.text) { if (packed.user.isCat && packed.user.speakAsCat && packed.text) {
const tokens = packed.text ? mfm.parse(packed.text) : []; const tokens = packed.text ? mfm.parse(packed.text) : [];
function nyaizeNode(node: mfm.MfmNode) { function nyaizeNode(node: mfm.MfmNode) {
if (node.type === "quote") return; if (node.type === "quote") return;

View File

@ -438,6 +438,7 @@ export const UserRepository = db.getRepository(User).extend({
isModerator: user.isModerator || falsy, isModerator: user.isModerator || falsy,
isBot: user.isBot || falsy, isBot: user.isBot || falsy,
isCat: user.isCat || falsy, isCat: user.isCat || falsy,
speakAsCat: user.speakAsCat || falsy,
instance: user.host instance: user.host
? userInstanceCache ? userInstanceCache
.fetch( .fetch(

View File

@ -66,6 +66,11 @@ export const packedUserLiteSchema = {
nullable: false, nullable: false,
optional: true, optional: true,
}, },
speakAsCat: {
type: "boolean",
nullable: false,
optional: true,
},
emojis: { emojis: {
type: "array", type: "array",
nullable: false, nullable: false,

View File

@ -1,4 +1,3 @@
import bcrypt from "bcryptjs";
import { generateKeyPair } from "node:crypto"; import { generateKeyPair } from "node:crypto";
import generateUserToken from "./generate-native-user-token.js"; import generateUserToken from "./generate-native-user-token.js";
import { User } from "@/models/entities/user.js"; import { User } from "@/models/entities/user.js";
@ -12,6 +11,7 @@ import { usersChart } from "@/services/chart/index.js";
import { UsedUsername } from "@/models/entities/used-username.js"; import { UsedUsername } from "@/models/entities/used-username.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import config from "@/config/index.js"; import config from "@/config/index.js";
import { hashPassword } from "@/misc/password.js";
export async function signup(opts: { export async function signup(opts: {
username: User["username"]; username: User["username"];
@ -42,8 +42,7 @@ export async function signup(opts: {
} }
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); hash = await hashPassword(password);
hash = await bcrypt.hash(password, salt);
} }
// Generate secret // Generate secret

View File

@ -290,6 +290,8 @@ import * as ep___resetDb from "./endpoints/reset-db.js";
import * as ep___resetPassword from "./endpoints/reset-password.js"; import * as ep___resetPassword from "./endpoints/reset-password.js";
import * as ep___serverInfo from "./endpoints/server-info.js"; import * as ep___serverInfo from "./endpoints/server-info.js";
import * as ep___stats from "./endpoints/stats.js"; import * as ep___stats from "./endpoints/stats.js";
import * as ep___sw_show_registration from './endpoints/sw/show-registration.js';
import * as ep___sw_update_registration from './endpoints/sw/update-registration.js';
import * as ep___sw_register from "./endpoints/sw/register.js"; import * as ep___sw_register from "./endpoints/sw/register.js";
import * as ep___sw_unregister from "./endpoints/sw/unregister.js"; import * as ep___sw_unregister from "./endpoints/sw/unregister.js";
import * as ep___test from "./endpoints/test.js"; import * as ep___test from "./endpoints/test.js";
@ -637,6 +639,8 @@ const eps = [
["stats", ep___stats], ["stats", ep___stats],
["sw/register", ep___sw_register], ["sw/register", ep___sw_register],
["sw/unregister", ep___sw_unregister], ["sw/unregister", ep___sw_unregister],
['sw/show-registration', ep___sw_show_registration],
['sw/update-registration', ep___sw_update_registration],
["test", ep___test], ["test", ep___test],
["username/available", ep___username_available], ["username/available", ep___username_available],
["users", ep___users], ["users", ep___users],

View File

@ -1,7 +1,8 @@
import define from "../../define.js"; import define from "../../define.js";
import bcrypt from "bcryptjs"; // import bcrypt from "bcryptjs";
import rndstr from "rndstr"; import rndstr from "rndstr";
import { Users, UserProfiles } from "@/models/index.js"; import { Users, UserProfiles } from "@/models/index.js";
import { hashPassword } from "@/misc/password.js";
export const meta = { export const meta = {
tags: ["admin"], tags: ["admin"],
@ -47,7 +48,8 @@ export default define(meta, paramDef, async (ps) => {
const passwd = rndstr("a-zA-Z0-9", 8); const passwd = rndstr("a-zA-Z0-9", 8);
// Generate hash of password // Generate hash of password
const hash = bcrypt.hashSync(passwd); // const hash = bcrypt.hashSync(passwd);
const hash = await hashPassword(passwd);
await UserProfiles.update( await UserProfiles.update(
{ {

View File

@ -209,7 +209,12 @@ export default define(meta, paramDef, async (ps, me) => {
} }
if (Array.isArray(ps.blockedHosts)) { if (Array.isArray(ps.blockedHosts)) {
set.blockedHosts = ps.blockedHosts.filter(Boolean); let lastValue = '';
set.blockedHosts = ps.blockedHosts.sort().filter(h => {
const lv = lastValue;
lastValue = h;
return h !== '' && h !== lv;
});
} }
if (ps.themeColor !== undefined) { if (ps.themeColor !== undefined) {

View File

@ -1,4 +1,3 @@
import bcrypt from "bcryptjs";
import { promisify } from "node:util"; import { promisify } from "node:util";
import * as cbor from "cbor"; import * as cbor from "cbor";
import define from "../../../define.js"; import define from "../../../define.js";
@ -11,6 +10,7 @@ import {
import config from "@/config/index.js"; import config from "@/config/index.js";
import { procedures, hash } from "../../../2fa.js"; import { procedures, hash } from "../../../2fa.js";
import { publishMainStream } from "@/services/stream.js"; import { publishMainStream } from "@/services/stream.js";
import { comparePassword } from "@/misc/password.js";
const cborDecodeFirst = promisify(cbor.decodeFirst) as any; const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8")); const rpIdHashReal = hash(Buffer.from(config.hostname, "utf-8"));
@ -43,7 +43,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -1,10 +1,10 @@
import bcrypt from "bcryptjs";
import define from "../../../define.js"; import define from "../../../define.js";
import { UserProfiles, AttestationChallenges } from "@/models/index.js"; import { UserProfiles, AttestationChallenges } from "@/models/index.js";
import { promisify } from "node:util"; import { promisify } from "node:util";
import * as crypto from "node:crypto"; import * as crypto from "node:crypto";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { hash } from "../../../2fa.js"; import { hash } from "../../../2fa.js";
import { comparePassword } from "@/misc/password.js";
const randomBytes = promisify(crypto.randomBytes); const randomBytes = promisify(crypto.randomBytes);
@ -26,7 +26,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -1,9 +1,9 @@
import bcrypt from "bcryptjs";
import * as speakeasy from "speakeasy"; import * as speakeasy from "speakeasy";
import * as QRCode from "qrcode"; import * as QRCode from "qrcode";
import config from "@/config/index.js"; import config from "@/config/index.js";
import { UserProfiles } from "@/models/index.js"; import { UserProfiles } from "@/models/index.js";
import define from "../../../define.js"; import define from "../../../define.js";
import { comparePassword } from "@/misc/password.js";
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -23,7 +23,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -1,4 +1,4 @@
import bcrypt from "bcryptjs"; import { comparePassword } from "@/misc/password.js";
import define from "../../../define.js"; import define from "../../../define.js";
import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js"; import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.js";
import { publishMainStream } from "@/services/stream.js"; import { publishMainStream } from "@/services/stream.js";
@ -22,7 +22,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -1,6 +1,6 @@
import bcrypt from "bcryptjs";
import define from "../../../define.js"; import define from "../../../define.js";
import { UserProfiles } from "@/models/index.js"; import { UserProfiles } from "@/models/index.js";
import { comparePassword } from "@/misc/password.js";
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -20,7 +20,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -1,6 +1,6 @@
import bcrypt from "bcryptjs";
import define from "../../define.js"; import define from "../../define.js";
import { UserProfiles } from "@/models/index.js"; import { UserProfiles } from "@/models/index.js";
import { hashPassword, comparePassword } from "@/misc/password.js";
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -21,15 +21,14 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.currentPassword, profile.password!); const same = await comparePassword(ps.currentPassword, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");
} }
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const hash = await hashPassword(ps.newPassword);
const hash = await bcrypt.hash(ps.newPassword, salt);
await UserProfiles.update(user.id, { await UserProfiles.update(user.id, {
password: hash, password: hash,

View File

@ -1,7 +1,7 @@
import bcrypt from "bcryptjs";
import { UserProfiles, Users } from "@/models/index.js"; import { UserProfiles, Users } from "@/models/index.js";
import { deleteAccount } from "@/services/delete-account.js"; import { deleteAccount } from "@/services/delete-account.js";
import define from "../../define.js"; import define from "../../define.js";
import { comparePassword } from "@/misc/password.js";
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -25,7 +25,7 @@ export default define(meta, paramDef, async (ps, user) => {
} }
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -1,4 +1,3 @@
import bcrypt from "bcryptjs";
import { import {
publishInternalEvent, publishInternalEvent,
publishMainStream, publishMainStream,
@ -7,6 +6,7 @@ import {
import generateUserToken from "../../common/generate-native-user-token.js"; import generateUserToken from "../../common/generate-native-user-token.js";
import define from "../../define.js"; import define from "../../define.js";
import { Users, UserProfiles } from "@/models/index.js"; import { Users, UserProfiles } from "@/models/index.js";
import { comparePassword } from "@/misc/password.js";
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -29,7 +29,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new Error("incorrect password"); throw new Error("incorrect password");

View File

@ -2,12 +2,12 @@ import { publishMainStream } from "@/services/stream.js";
import define from "../../define.js"; import define from "../../define.js";
import rndstr from "rndstr"; import rndstr from "rndstr";
import config from "@/config/index.js"; import config from "@/config/index.js";
import bcrypt from "bcryptjs";
import { Users, UserProfiles } from "@/models/index.js"; import { Users, UserProfiles } from "@/models/index.js";
import { sendEmail } from "@/services/send-email.js"; import { sendEmail } from "@/services/send-email.js";
import { ApiError } from "../../error.js"; import { ApiError } from "../../error.js";
import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
import { HOUR } from "@/const.js"; import { HOUR } from "@/const.js";
import { comparePassword } from "@/misc/password.js";
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
@ -47,7 +47,7 @@ export default define(meta, paramDef, async (ps, user) => {
const profile = await UserProfiles.findOneByOrFail({ userId: user.id }); const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
// Compare password // Compare password
const same = await bcrypt.compare(ps.password, profile.password!); const same = await comparePassword(ps.password, profile.password!);
if (!same) { if (!same) {
throw new ApiError(meta.errors.incorrectPassword); throw new ApiError(meta.errors.incorrectPassword);

View File

@ -104,6 +104,7 @@ export const paramDef = {
noCrawle: { type: "boolean" }, noCrawle: { type: "boolean" },
isBot: { type: "boolean" }, isBot: { type: "boolean" },
isCat: { type: "boolean" }, isCat: { type: "boolean" },
speakAsCat: { type: "boolean" },
showTimelineReplies: { type: "boolean" }, showTimelineReplies: { type: "boolean" },
injectFeaturedNote: { type: "boolean" }, injectFeaturedNote: { type: "boolean" },
receiveAnnouncementEmail: { type: "boolean" }, receiveAnnouncementEmail: { type: "boolean" },
@ -191,6 +192,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed; profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle; if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle;
if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat; if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
if (typeof ps.injectFeaturedNote === "boolean") if (typeof ps.injectFeaturedNote === "boolean")
profileUpdates.injectFeaturedNote = ps.injectFeaturedNote; profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
if (typeof ps.receiveAnnouncementEmail === "boolean") if (typeof ps.receiveAnnouncementEmail === "boolean")

View File

@ -93,13 +93,27 @@ export default define(meta, paramDef, async (ps, user) => {
} }
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {
if (user) { if (user) {
activeUsersChart.read(user); activeUsersChart.read(user);
} }
}); });
return await Notes.packMany(timeline, user); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -151,11 +151,25 @@ export default define(meta, paramDef, async (ps, user) => {
} }
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {
activeUsersChart.read(user); activeUsersChart.read(user);
}); });
return await Notes.packMany(timeline, user); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -123,13 +123,27 @@ export default define(meta, paramDef, async (ps, user) => {
} }
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {
if (user) { if (user) {
activeUsersChart.read(user); activeUsersChart.read(user);
} }
}); });
return await Notes.packMany(timeline, user); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -86,9 +86,24 @@ export default define(meta, paramDef, async (ps, user) => {
query.setParameters(followingQuery.getParameters()); query.setParameters(followingQuery.getParameters());
} }
const mentions = await query.take(ps.limit).getMany();
read(user.id, mentions); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
return await Notes.packMany(mentions, user); if (found.length > ps.limit) {
found.length = ps.limit;
}
read(user.id, found);
return found;
}); });

View File

@ -126,13 +126,27 @@ export default define(meta, paramDef, async (ps, user) => {
} }
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {
if (user) { if (user) {
activeUsersChart.read(user); activeUsersChart.read(user);
} }
}); });
return await Notes.packMany(timeline, user); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -74,7 +74,21 @@ export default define(meta, paramDef, async (ps, user) => {
if (user) generateMutedUserQuery(query, user); if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user); if (user) generateBlockedUserQuery(query, user);
const renotes = await query.take(ps.limit).getMany(); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
return await Notes.packMany(renotes, user); if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -58,7 +58,21 @@ export default define(meta, paramDef, async (ps, user) => {
if (user) generateMutedUserQuery(query, user); if (user) generateMutedUserQuery(query, user);
if (user) generateBlockedUserQuery(query, user); if (user) generateBlockedUserQuery(query, user);
const timeline = await query.take(ps.limit).getMany(); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
return await Notes.packMany(timeline, user); if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -145,8 +145,21 @@ export default define(meta, paramDef, async (ps, me) => {
} }
} }
// Search notes // We fetch more than requested because some may be filtered out, and if there's less than
const notes = await query.take(ps.limit).getMany(); // requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, me))
skip += take;
if (notes.length < take) break;
}
return await Notes.packMany(notes, me); if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -143,11 +143,25 @@ export default define(meta, paramDef, async (ps, user) => {
} }
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany();
process.nextTick(() => { process.nextTick(() => {
activeUsersChart.read(user); activeUsersChart.read(user);
}); });
return await Notes.packMany(timeline, user); // We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -138,9 +138,27 @@ export default define(meta, paramDef, async (ps, user) => {
} }
//#endregion //#endregion
const timeline = await query.take(ps.limit).getMany(); process.nextTick(() => {
if (user) {
activeUsersChart.read(user); activeUsersChart.read(user);
}
return await Notes.packMany(timeline, user); });
// We fetch more than requested because some may be filtered out, and if there's less than
// requested, the pagination stops.
const found = [];
const take = Math.floor(ps.limit * 1.5);
let skip = 0;
while (found.length < ps.limit) {
const notes = await query.take(take).skip(skip).getMany();
found.push(...await Notes.packMany(notes, user))
skip += take;
if (notes.length < take) break;
}
if (found.length > ps.limit) {
found.length = ps.limit;
}
return found;
}); });

View File

@ -1,8 +1,8 @@
import bcrypt from "bcryptjs";
import { publishMainStream } from "@/services/stream.js"; import { publishMainStream } from "@/services/stream.js";
import { Users, UserProfiles, PasswordResetRequests } from "@/models/index.js"; import { Users, UserProfiles, PasswordResetRequests } from "@/models/index.js";
import define from "../define.js"; import define from "../define.js";
import { ApiError } from "../error.js"; import { ApiError } from "../error.js";
import { hashPassword } from "@/misc/password.js";
export const meta = { export const meta = {
tags: ["reset password"], tags: ["reset password"],
@ -34,8 +34,7 @@ export default define(meta, paramDef, async (ps, user) => {
} }
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const hash = await hashPassword(ps.password);
const hash = await bcrypt.hash(ps.password, salt);
await UserProfiles.update(req.userId, { await UserProfiles.update(req.userId, {
password: hash, password: hash,

View File

@ -26,6 +26,18 @@ export const meta = {
optional: false, optional: false,
nullable: true, nullable: true,
}, },
userId: {
type: 'string',
optional: false, nullable: false,
},
endpoint: {
type: 'string',
optional: false, nullable: false,
},
sendReadMessage: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
}, },
} as const; } as const;
@ -36,14 +48,15 @@ export const paramDef = {
endpoint: { type: "string" }, endpoint: { type: "string" },
auth: { type: "string" }, auth: { type: "string" },
publickey: { type: "string" }, publickey: { type: "string" },
sendReadMessage: { type: 'boolean', default: false },
}, },
required: ["endpoint", "auth", "publickey"], required: ["endpoint", "auth", "publickey"],
} as const; } as const;
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, me) => {
// if already subscribed // if already subscribed
const exist = await SwSubscriptions.findOneBy({ const exist = await SwSubscriptions.findOneBy({
userId: user.id, userId: me.id,
endpoint: ps.endpoint, endpoint: ps.endpoint,
auth: ps.auth, auth: ps.auth,
publickey: ps.publickey, publickey: ps.publickey,
@ -55,20 +68,27 @@ export default define(meta, paramDef, async (ps, user) => {
return { return {
state: "already-subscribed" as const, state: "already-subscribed" as const,
key: instance.swPublicKey, key: instance.swPublicKey,
userId: me.id,
endpoint: exist.endpoint,
sendReadMessage: exist.sendReadMessage,
}; };
} }
await SwSubscriptions.insert({ await SwSubscriptions.insert({
id: genId(), id: genId(),
createdAt: new Date(), createdAt: new Date(),
userId: user.id, userId: me.id,
endpoint: ps.endpoint, endpoint: ps.endpoint,
auth: ps.auth, auth: ps.auth,
publickey: ps.publickey, publickey: ps.publickey,
sendReadMessage: ps.sendReadMessage,
}); });
return { return {
state: "subscribed" as const, state: "subscribed" as const,
key: instance.swPublicKey, key: instance.swPublicKey,
userId: me.id,
endpoint: ps.endpoint,
sendReadMessage: ps.sendReadMessage,
}; };
}); });

View File

@ -0,0 +1,55 @@
import { SwSubscriptions } from '@/models/index.js';
import define from "../../define.js";
export const meta = {
tags: ['account'],
requireCredential: true,
description: 'Check push notification registration exists.',
res: {
type: 'object',
optional: false, nullable: true,
properties: {
userId: {
type: 'string',
optional: false, nullable: false,
},
endpoint: {
type: 'string',
optional: false, nullable: false,
},
sendReadMessage: {
type: 'boolean',
optional: false, nullable: false,
},
},
},
} as const;
export const paramDef = {
type: 'object',
properties: {
endpoint: { type: 'string' },
},
required: ['endpoint'],
} as const;
// eslint-disable-next-line import/no-default-export
export default define(meta, paramDef, async (ps, me) => {
const exist = await SwSubscriptions.findOneBy({
userId: me.id,
endpoint: ps.endpoint,
});
if (exist != null) {
return {
userId: exist.userId,
endpoint: exist.endpoint,
sendReadMessage: exist.sendReadMessage,
};
}
return null;
});

View File

@ -4,7 +4,7 @@ import define from "../../define.js";
export const meta = { export const meta = {
tags: ["account"], tags: ["account"],
requireCredential: true, requireCredential: false,
description: "Unregister from receiving push notifications.", description: "Unregister from receiving push notifications.",
} as const; } as const;
@ -17,9 +17,9 @@ export const paramDef = {
required: ["endpoint"], required: ["endpoint"],
} as const; } as const;
export default define(meta, paramDef, async (ps, user) => { export default define(meta, paramDef, async (ps, me) => {
await SwSubscriptions.delete({ await SwSubscriptions.delete({
userId: user.id, ...(me ? { userId: me.id } : {}),
endpoint: ps.endpoint, endpoint: ps.endpoint,
}); });
}); });

View File

@ -0,0 +1,44 @@
import { SwSubscriptions } from "@/models/index.js";
import define from "../../define.js";
export const meta = {
tags: ["account"],
requireCredential: true,
description: "Unregister from receiving push notifications.",
} as const;
export const paramDef = {
type: "object",
properties: {
endpoint: { type: "string" },
sendReadMessage: { type: 'boolean' },
},
required: ["endpoint"],
} as const;
export default define(meta, paramDef, async (ps, me) => {
const swSubscription = await SwSubscriptions.findOneBy({
userId: me.id,
endpoint: ps.endpoint,
});
if (swSubscription === null) {
throw new Error("No such registration");
}
if (ps.sendReadMessage !== undefined) {
swSubscription.sendReadMessage = ps.sendReadMessage;
}
await SwSubscriptions.update(swSubscription.id, {
sendReadMessage: swSubscription.sendReadMessage,
});
return {
userId: swSubscription.userId,
endpoint: swSubscription.endpoint,
sendReadMessage: swSubscription.sendReadMessage,
};
});

View File

@ -1,5 +1,4 @@
import type Koa from "koa"; import type Koa from "koa";
import bcrypt from "bcryptjs";
import * as speakeasy from "speakeasy"; import * as speakeasy from "speakeasy";
import signin from "../common/signin.js"; import signin from "../common/signin.js";
import config from "@/config/index.js"; import config from "@/config/index.js";

View File

@ -1,6 +1,5 @@
import type Koa from "koa"; import type Koa from "koa";
import rndstr from "rndstr"; import rndstr from "rndstr";
import bcrypt from "bcryptjs";
import { fetchMeta } from "@/misc/fetch-meta.js"; import { fetchMeta } from "@/misc/fetch-meta.js";
import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js"; import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.js";
import { Users, RegistrationTickets, UserPendings } from "@/models/index.js"; import { Users, RegistrationTickets, UserPendings } from "@/models/index.js";
@ -9,6 +8,7 @@ import config from "@/config/index.js";
import { sendEmail } from "@/services/send-email.js"; import { sendEmail } from "@/services/send-email.js";
import { genId } from "@/misc/gen-id.js"; import { genId } from "@/misc/gen-id.js";
import { validateEmailForAccount } from "@/services/validate-email-for-account.js"; import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
import { hashPassword } from "@/misc/password.js";
export default async (ctx: Koa.Context) => { export default async (ctx: Koa.Context) => {
const body = ctx.request.body; const body = ctx.request.body;
@ -79,8 +79,7 @@ export default async (ctx: Koa.Context) => {
const code = rndstr("a-z0-9", 16); const code = rndstr("a-z0-9", 16);
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const hash = await hashPassword(password);
const hash = await bcrypt.hash(password, salt);
await UserPendings.insert({ await UserPendings.insert({
id: genId(), id: genId(),

View File

@ -1,4 +1,3 @@
import bcrypt from "bcryptjs";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import generateNativeUserToken from "../server/api/common/generate-native-user-token.js"; import generateNativeUserToken from "../server/api/common/generate-native-user-token.js";
import { genRsaKeyPair } from "@/misc/gen-key-pair.js"; import { genRsaKeyPair } from "@/misc/gen-key-pair.js";
@ -9,13 +8,13 @@ import { genId } from "@/misc/gen-id.js";
import { UserKeypair } from "@/models/entities/user-keypair.js"; import { UserKeypair } from "@/models/entities/user-keypair.js";
import { UsedUsername } from "@/models/entities/used-username.js"; import { UsedUsername } from "@/models/entities/used-username.js";
import { db } from "@/db/postgre.js"; import { db } from "@/db/postgre.js";
import { hashPassword } from "@/misc/password.js";
export async function createSystemUser(username: string) { export async function createSystemUser(username: string) {
const password = uuid(); const password = uuid();
// Generate hash of password // Generate hash of password
const salt = await bcrypt.genSalt(8); const hash = await hashPassword(password);
const hash = await bcrypt.hash(password, salt);
// Generate secret // Generate secret
const secret = generateNativeUserToken(); const secret = generateNativeUserToken();

View File

@ -63,6 +63,13 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(
}); });
for (const subscription of subscriptions) { for (const subscription of subscriptions) {
if ([
'readNotifications',
'readAllNotifications',
'readAllMessagingMessages',
'readAllMessagingMessagesOfARoom',
].includes(type) && !subscription.sendReadMessage) continue;
const pushSubscription = { const pushSubscription = {
endpoint: subscription.endpoint, endpoint: subscription.endpoint,
keys: { keys: {

View File

@ -141,7 +141,7 @@ onBeforeUnmount(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.kpoogebi { .kpoogebi {
position: relative; position: relative;
display: flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-weight: bold; font-weight: bold;

View File

@ -10,16 +10,16 @@
:class="{ renote: isRenote }" :class="{ renote: isRenote }"
> >
<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/> <MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" class="reply-to"/>
<div class="note-context"> <div class="note-context" @click="noteClick">
<div class="line"></div> <div class="line"></div>
<div v-if="appearNote._prId_" class="info"><i class="ph-megaphone-simple-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div> <div v-if="appearNote._prId_" class="info"><i class="ph-megaphone-simple-bold ph-lg"></i> {{ i18n.ts.promotion }}<button class="_textButton hide" @click.stop="readPromo()">{{ i18n.ts.hideThisNote }} <i class="ph-x ph-bold ph-lg"></i></button></div>
<div v-if="appearNote._featuredId_" class="info"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div> <div v-if="appearNote._featuredId_" class="info"><i class="ph-lightning ph-bold ph-lg"></i> {{ i18n.ts.featured }}</div>
<div v-if="pinned" class="info"><i class="ph-push-pin ph-bold ph-lg"></i>{{ i18n.ts.pinnedNote }}</div> <div v-if="pinned" class="info"><i class="ph-push-pin ph-bold ph-lg"></i>{{ i18n.ts.pinnedNote }}</div>
<div v-if="isRenote" class="renote"> <div v-if="isRenote" class="renote">
<i class="ph-repeat ph-bold ph-lg"></i> <i class="ph-repeat ph-bold ph-lg"></i>
<I18n :src="i18n.ts.renotedBy" tag="span"> <I18n :src="i18n.ts.renotedBy" tag="span">
<template #user> <template #user>
<MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)"> <MkA v-user-preview="note.userId" class="name" :to="userPage(note.user)" @click.stop>
<MkUserName :user="note.user"/> <MkUserName :user="note.user"/>
</MkA> </MkA>
</template> </template>
@ -93,7 +93,6 @@
<i class="ph-dots-three-outline ph-bold ph-lg"></i> <i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button> </button>
</footer> </footer>
<!-- <MkNoteFooter :note="appearNote"></MkNoteFooter> -->
</div> </div>
</article> </article>
</div> </div>
@ -418,7 +417,7 @@ function readPromo() {
align-items: center; align-items: center;
white-space: pre; white-space: pre;
color: var(--renote); color: var(--renote);
cursor: pointer;
> i { > i {
margin-right: 4px; margin-right: 4px;
@ -504,7 +503,7 @@ function readPromo() {
width: 100%; width: 100%;
margin-top: 1em; margin-top: 1em;
position: sticky; position: sticky;
bottom: 1em; bottom: var(--stickyBottom);
> span { > span {
display: inline-block; display: inline-block;
@ -663,6 +662,9 @@ function readPromo() {
} }
> .line { > .line {
margin-right: 10px; margin-right: 10px;
&::before {
margin-top: 8px;
}
} }
} }
> .article { > .article {

View File

@ -352,6 +352,7 @@ onUnmounted(() => {
<style lang="scss" scoped> <style lang="scss" scoped>
.lxwezrsl { .lxwezrsl {
font-size: 1.05em;
position: relative; position: relative;
transition: box-shadow 0.1s ease; transition: box-shadow 0.1s ease;
contain: content; contain: content;
@ -451,7 +452,7 @@ onUnmounted(() => {
&:last-child { &:last-child {
padding-bottom: 24px; padding-bottom: 24px;
} }
font-size: 1.2em; font-size: 1.1em;
overflow: clip; overflow: clip;
outline: none; outline: none;
scroll-margin-top: calc(var(--stickyTop) + 20vh); scroll-margin-top: calc(var(--stickyTop) + 20vh);

View File

@ -7,25 +7,25 @@
<div v-if="conversation && depth > 1" class="line"></div> <div v-if="conversation && depth > 1" class="line"></div>
<div class="main" @click="noteClick"> <div class="main" @click="noteClick">
<div class="avatar-container"> <div class="avatar-container">
<MkAvatar class="avatar" :user="note.user"/> <MkAvatar class="avatar" :user="appearNote.user"/>
<div v-if="(!conversation) || replies.length > 0" class="line"></div> <div v-if="(!conversation) || replies.length > 0" class="line"></div>
</div> </div>
<div class="body"> <div class="body">
<XNoteHeader class="header" :note="note" :mini="true"/> <XNoteHeader class="header" :note="note" :mini="true"/>
<div class="body"> <div class="body">
<p v-if="note.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<MkA v-if="note.replyId" :to="`/notes/${note.replyId}`" class="reply-icon" @click.stop> <MkA v-if="appearNote.replyId" :to="`/notes/${appearNote.replyId}`" class="reply-icon" @click.stop>
<i class="ph-arrow-bend-left-up ph-bold ph-lg"></i> <i class="ph-arrow-bend-left-up ph-bold ph-lg"></i>
</MkA> </MkA>
<MkA v-if="conversation && note.renoteId && note.renoteId != parentId && !note.replyId" :to="`/notes/${note.renoteId}`" class="reply-icon" @click.stop> <MkA v-if="conversation && appearNote.renoteId && appearNote.renoteId != parentId && !appearNote.replyId" :to="`/notes/${appearNote.renoteId}`" class="reply-icon" @click.stop>
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</MkA> </MkA>
<Mfm v-if="note.cw != ''" class="text" :text="note.cw" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<br/> <br/>
<XCwButton v-model="showContent" :note="note"/> <XCwButton v-model="showContent" :note="note"/>
</p> </p>
<div v-show="note.cw == null || showContent" class="content"> <div v-show="appearNote.cw == null || showContent" class="content">
<MkSubNoteContent class="text" :note="note" :detailed="true" :parentId="note.parentId" :conversation="conversation"/> <MkSubNoteContent class="text" :note="note" :detailed="true" :parentId="appearNote.parentId" :conversation="conversation"/>
</div> </div>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
@ -56,15 +56,14 @@
<i class="ph-dots-three-outline ph-bold ph-lg"></i> <i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button> </button>
</footer> </footer>
<!-- <MkNoteFooter :note="note" :directReplies="replies.length"></MkNoteFooter> -->
</div> </div>
</div> </div>
<template v-if="conversation"> <template v-if="conversation">
<template v-if="replies.length == 1"> <template v-if="replies.length == 1">
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply single" :conversation="conversation" :depth="depth" :parentId="note.replyId"/> <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply single" :conversation="conversation" :depth="depth" :parentId="appearNote.replyId"/>
</template> </template>
<template v-else-if="depth < 5"> <template v-else-if="depth < 5">
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1" :parentId="note.replyId"/> <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1" :parentId="appearNote.replyId"/>
</template> </template>
<div v-else-if="replies.length > 0" class="more"> <div v-else-if="replies.length > 0" class="more">
<div class="line"></div> <div class="line"></div>
@ -457,6 +456,26 @@ function noteClick(e) {
-webkit-mask: linear-gradient(to right, transparent 2px, black 2px); -webkit-mask: linear-gradient(to right, transparent 2px, black 2px);
} }
} }
// End Reply Divider
.children > .main:last-child {
padding-bottom: 1em;
&::before {
bottom: 1em;
}
// &::after {
// content: "";
// border-top: 1px solid var(--X13);
// position: absolute;
// bottom: 0;
// margin-left: calc(var(--avatarSize) + 12px);
// inset-inline: 0;
// }
}
&.firstColumn > .children:last-child > .main {
padding-bottom: 0 !important;
&::before { bottom: 0 !important }
// &::after { content: unset }
}
&.max-width_500px { &.max-width_500px {
:not(.reply) > & { :not(.reply) > & {

View File

@ -0,0 +1,171 @@
<template>
<MkButton
v-if="supported && !pushRegistrationInServer"
type="button"
primary
:gradate="gradate"
:rounded="rounded"
:inline="inline"
:autofocus="autofocus"
:wait="wait"
:full="full"
@click="subscribe"
>
{{ i18n.ts.subscribePushNotification }}
</MkButton>
<MkButton
v-else-if="!showOnlyToRegister && ($i ? pushRegistrationInServer : pushSubscription)"
type="button"
:primary="false"
:gradate="gradate"
:rounded="rounded"
:inline="inline"
:autofocus="autofocus"
:wait="wait"
:full="full"
@click="unsubscribe"
>
{{ i18n.ts.unsubscribePushNotification }}
</MkButton>
<MkButton v-else-if="$i && pushRegistrationInServer" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
{{ i18n.ts.pushNotificationAlreadySubscribed }}
</MkButton>
<MkButton v-else-if="!supported" disabled :rounded="rounded" :inline="inline" :wait="wait" :full="full">
{{ i18n.ts.pushNotificationNotSupported }}
</MkButton>
</template>
<script setup lang="ts">
import { $i, getAccounts } from '@/account';
import MkButton from '@/components/MkButton.vue';
import { instance } from '@/instance';
import { api, apiWithDialog, promiseDialog } from '@/os';
import { i18n } from '@/i18n';
defineProps<{
primary?: boolean;
gradate?: boolean;
rounded?: boolean;
inline?: boolean;
link?: boolean;
to?: string;
autofocus?: boolean;
wait?: boolean;
danger?: boolean;
full?: boolean;
showOnlyToRegister?: boolean;
}>();
// ServiceWorker registration
let registration = $ref<ServiceWorkerRegistration | undefined>();
// If this browser supports push notification
let supported = $ref(false);
// If this browser has already subscribed to push notification
let pushSubscription = $ref<PushSubscription | null>(null);
let pushRegistrationInServer = $ref<{ state?: string; key?: string; userId: string; endpoint: string; sendReadMessage: boolean; } | undefined>();
function subscribe() {
if (!registration || !supported || !instance.swPublickey) return;
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
return promiseDialog(registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
.then(async subscription => {
pushSubscription = subscription;
// Register
pushRegistrationInServer = await api('sw/register', {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey('auth')),
publickey: encode(subscription.getKey('p256dh')),
});
}, async err => { // When subscribe failed
//
if (err?.name === 'NotAllowedError') {
console.info('User denied the notification permission request.');
return;
}
// applicationServerKey ( gcm_sender_id)
//
//
//
await unsubscribe();
}), null, null);
}
async function unsubscribe() {
if (!pushSubscription) return;
const endpoint = pushSubscription.endpoint;
const accounts = await getAccounts();
pushRegistrationInServer = undefined;
if ($i && accounts.length >= 2) {
apiWithDialog('sw/unregister', {
i: $i.token,
endpoint,
});
} else {
pushSubscription.unsubscribe();
apiWithDialog('sw/unregister', {
endpoint,
});
pushSubscription = null;
}
}
function encode(buffer: ArrayBuffer | null) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
/**
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding)
.replace(/-/g, '+')
.replace(/_/g, '/');
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}
if (navigator.serviceWorker == null) {
// TODO:
} else {
navigator.serviceWorker.ready.then(async swr => {
registration = swr;
pushSubscription = await registration.pushManager.getSubscription();
if (instance.swPublickey && ('PushManager' in window) && $i && $i.token) {
supported = true;
if (pushSubscription) {
const res = await api('sw/show-registration', {
endpoint: pushSubscription.endpoint,
});
if (res) {
pushRegistrationInServer = res;
}
}
}
});
}
defineExpose({
pushRegistrationInServer: $$(pushRegistrationInServer),
});
</script>

View File

@ -130,7 +130,7 @@ const urls = props.note.text ? extractUrlFromMfm(mfm.parse(props.note.text)) : n
width: 100%; width: 100%;
margin-top: 1em; margin-top: 1em;
position: sticky; position: sticky;
bottom: 1em; bottom: var(--stickyBottom);
> span { > span {
display: inline-block; display: inline-block;

View File

@ -106,6 +106,7 @@
<MkSparkle> <MkSparkle>
<h3>{{ i18n.ts._tutorial.step6_4 }} <Mfm text="$[shake 🚀]"></Mfm></h3> <h3>{{ i18n.ts._tutorial.step6_4 }} <Mfm text="$[shake 🚀]"></Mfm></h3>
</MkSparkle> </MkSparkle>
<MkPushNotificationAllowButton primary show-only-to-register/>
</div> </div>
</Transition> </Transition>
</div> </div>
@ -122,6 +123,7 @@ import MkButton from '@/components/MkButton.vue';
import XFeaturedUsers from '@/pages/explore.users.vue'; import XFeaturedUsers from '@/pages/explore.users.vue';
import XPostForm from '@/components/MkPostForm.vue'; import XPostForm from '@/components/MkPostForm.vue';
import MkSparkle from '@/components/MkSparkle.vue'; import MkSparkle from '@/components/MkSparkle.vue';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { defaultStore } from '@/store'; import { defaultStore } from '@/store';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { $i } from '@/account'; import { $i } from '@/account';

View File

@ -35,8 +35,10 @@
<FormSection v-if="iAmModerator"> <FormSection v-if="iAmModerator">
<template #label>Moderation</template> <template #label>Moderation</template>
<FormSuspense :p="init">
<FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch> <FormSwitch v-model="suspended" class="_formBlock" @update:modelValue="toggleSuspend">{{ i18n.ts.stopActivityDelivery }}</FormSwitch>
<FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch> <FormSwitch v-model="isBlocked" class="_formBlock" @update:modelValue="toggleBlock">{{ i18n.ts.blockThisInstance }}</FormSwitch>
</FormSuspense>
<MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton> <MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
</FormSection> </FormSection>
@ -158,6 +160,13 @@ import 'swiper/scss';
import 'swiper/scss/virtual'; import 'swiper/scss/virtual';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy'; import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
blockedHosts: string[];
};
type AugmentedInstance = misskey.entities.Instance & {
isBlocked: boolean;
};
const props = defineProps<{ const props = defineProps<{
host: string; host: string;
}>(); }>();
@ -168,8 +177,8 @@ let tab = $ref(tabs[0]);
watch($$(tab), () => (syncSlide(tabs.indexOf(tab)))); watch($$(tab), () => (syncSlide(tabs.indexOf(tab))));
let chartSrc = $ref('instance-requests'); let chartSrc = $ref('instance-requests');
let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null); let meta = $ref<AugmentedInstanceMetadata | null>(null);
let instance = $ref<misskey.entities.Instance | null>(null); let instance = $ref<AugmentedInstance | null>(null);
let suspended = $ref(false); let suspended = $ref(false);
let isBlocked = $ref(false); let isBlocked = $ref(false);
let faviconUrl = $ref(null); let faviconUrl = $ref(null);
@ -185,19 +194,34 @@ const usersPagination = {
offsetMode: true, offsetMode: true,
}; };
async function fetch() { async function init() {
instance = await os.api('federation/show-instance', { meta = await os.api('admin/meta');
host: props.host,
});
suspended = instance.isSuspended;
isBlocked = instance.isBlocked;
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
} }
async function toggleBlock(ev) { async function fetch() {
instance = (await os.api('federation/show-instance', {
host: props.host,
})) as AugmentedInstance;
suspended = instance.isSuspended;
isBlocked = instance.isBlocked;
faviconUrl =
getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ??
getProxiedImageUrlNullable(instance.iconUrl, 'preview');
}
async function toggleBlock() {
if (meta == null) return; if (meta == null) return;
if (!instance) {
throw new Error(`Instance info not loaded`);
}
let blockedHosts: string[];
if (isBlocked) {
blockedHosts = meta.blockedHosts.concat([instance.host]);
} else {
blockedHosts = meta.blockedHosts.filter((x) => x !== instance!.host);
}
await os.api('admin/update-meta', { await os.api('admin/update-meta', {
blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host), blockedHosts,
}); });
} }

View File

@ -6,6 +6,21 @@
<FormButton class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormButton> <FormButton class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormButton>
<FormButton class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormButton> <FormButton class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormButton>
</FormSection> </FormSection>
<FormSection>
<template #label>{{ i18n.ts.pushNotification }}</template>
<div class="_gaps_m">
<MkPushNotificationAllowButton ref="allowButton"/>
<MkSwitch :disabled="!pushRegistrationInServer" :model-value="sendReadMessage" @update:model-value="onChangeSendReadMessage">
<template #label>{{ i18n.ts.sendPushNotificationReadMessage }}</template>
<template #caption>
<I18n :src="i18n.ts.sendPushNotificationReadMessageCaption">
<template #emptyPushNotificationMessage>{{ i18n.ts._notification.emptyPushNotificationMessage }}</template>
</I18n>
</template>
</MkSwitch>
</div>
</FormSection>
</div> </div>
</template> </template>
@ -19,6 +34,11 @@ import * as os from '@/os';
import { $i } from '@/account'; import { $i } from '@/account';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata'; import { definePageMetadata } from '@/scripts/page-metadata';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
let allowButton = $shallowRef<InstanceType<typeof MkPushNotificationAllowButton>>();
let pushRegistrationInServer = $computed(() => allowButton?.pushRegistrationInServer);
let sendReadMessage = $computed(() => pushRegistrationInServer?.sendReadMessage || false);
async function readAllUnreadNotes() { async function readAllUnreadNotes() {
await os.api('i/read-all-unread-notes'); await os.api('i/read-all-unread-notes');
@ -49,6 +69,18 @@ function configure() {
}, 'closed'); }, 'closed');
} }
function onChangeSendReadMessage(v: boolean) {
if (!pushRegistrationInServer) return;
os.apiWithDialog('sw/update-registration', {
endpoint: pushRegistrationInServer.endpoint,
sendReadMessage: v,
}).then(res => {
if (!allowButton) return;
allowButton.pushRegistrationInServer = res;
});
}
const headerActions = $computed(() => []); const headerActions = $computed(() => []);
const headerTabs = $computed(() => []); const headerTabs = $computed(() => []);

View File

@ -59,6 +59,7 @@
</FormSlot> </FormSlot>
<FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch> <FormSwitch v-model="profile.isCat" class="_formBlock">{{ i18n.ts.flagAsCat }}<template #caption>{{ i18n.ts.flagAsCatDescription }}</template></FormSwitch>
<FormSwitch v-if="profile.isCat" v-model="profile.speakAsCat" class="_formBlock">{{ i18n.ts.flagSpeakAsCat }}<template #caption>{{ i18n.ts.flagSpeakAsCatDescription }}</template></FormSwitch>
<FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch> <FormSwitch v-model="profile.showTimelineReplies" class="_formBlock">{{ i18n.ts.flagShowTimelineReplies }}<template #caption>{{ i18n.ts.flagShowTimelineRepliesDescription }} {{ i18n.ts.reflectMayTakeTime }}</template></FormSwitch>
<FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch> <FormSwitch v-model="profile.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
<div v-if="saveButton == true"> <div v-if="saveButton == true">
@ -92,6 +93,7 @@ const profile = reactive({
lang: $i?.lang, lang: $i?.lang,
isBot: $i?.isBot, isBot: $i?.isBot,
isCat: $i?.isCat, isCat: $i?.isCat,
speakAsCat: $i?.speakAsCat,
showTimelineReplies: $i?.showTimelineReplies, showTimelineReplies: $i?.showTimelineReplies,
}); });
@ -135,6 +137,7 @@ function save() {
lang: profile.lang || null, lang: profile.lang || null,
isBot: !!profile.isBot, isBot: !!profile.isBot,
isCat: !!profile.isCat, isCat: !!profile.isCat,
speakAsCat: !!profile.speakAsCat,
showTimelineReplies: !!profile.showTimelineReplies, showTimelineReplies: !!profile.showTimelineReplies,
}); });
} }

View File

@ -339,7 +339,7 @@ function syncSlide(index) {
} }
onMounted(() => { onMounted(() => {
syncSlide(timelines.indexOf(swiperRef.activeIndex)); syncSlide(timelines.indexOf(defaultStore.state.tl?.src || swiperRef.activeIndex));
}); });
// #v-ifdef VITE_CAPACITOR // #v-ifdef VITE_CAPACITOR

View File

@ -1,6 +1,3 @@
import { instance } from "@/instance";
import { $i } from "@/account";
import { api } from "@/os";
import { lang } from "@/config"; import { lang } from "@/config";
export async function initializeSw() { export async function initializeSw() {
@ -12,58 +9,5 @@ export async function initializeSw() {
msg: "initialize", msg: "initialize",
lang, lang,
}); });
if (instance.swPublickey && "PushManager" in window && $i && $i.token) {
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
registration.pushManager
.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
})
.then((subscription) => {
function encode(buffer: ArrayBuffer | null) {
return btoa(
String.fromCharCode.apply(null, new Uint8Array(buffer)),
);
}
// Register
api("sw/register", {
endpoint: subscription.endpoint,
auth: encode(subscription.getKey("auth")),
publickey: encode(subscription.getKey("p256dh")),
});
})
// When subscribe failed
.catch(async (err: Error) => {
// 通知が許可されていなかったとき
if (err.name === "NotAllowedError") {
return;
}
// 違うapplicationServerKey (または gcm_sender_id)のサブスクリプションが
// 既に存在していることが原因でエラーになった可能性があるので、
// そのサブスクリプションを解除しておく
const subscription = await registration.pushManager.getSubscription();
if (subscription) subscription.unsubscribe();
}); });
} }
});
}
/**
* Convert the URL safe base64 string to a Uint8Array
* @param base64String base64 string
*/
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
const rawData = window.atob(base64);
const outputArray = new Uint8Array(rawData.length);
for (let i = 0; i < rawData.length; ++i) {
outputArray[i] = rawData.charCodeAt(i);
}
return outputArray;
}

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="dkgtipfy" :class="{ wallpaper }"> <div class="dkgtipfy" :class="{ wallpaper, isMobile }">
<XSidebar v-if="!isMobile" class="sidebar"/> <XSidebar v-if="!isMobile" class="sidebar"/>
<MkStickyContainer class="contents"> <MkStickyContainer class="contents">
@ -319,6 +319,10 @@ console.log(mainRouter.currentRoute.value.name);
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
--stickyBottom: 1em;
&.isMobile {
--stickyBottom: 6rem;
}
&.wallpaper { &.wallpaper {
background: var(--wallpaperOverlay); background: var(--wallpaperOverlay);
//backdrop-filter: var(--blur, blur(4px)); //backdrop-filter: var(--blur, blur(4px));
@ -363,7 +367,7 @@ console.log(mainRouter.currentRoute.value.name);
} }
> .postButton, .widgetButton { > .postButton, .widgetButton {
bottom: 6rem; bottom: var(--stickyBottom);
right: 1.5rem; right: 1.5rem;
height: 4rem; height: 4rem;
width: 4rem; width: 4rem;

View File

@ -21,6 +21,7 @@
"@trapezial@calckey.jp", "@trapezial@calckey.jp",
"@unattributed@calckey.social", "@unattributed@calckey.social",
"@cody@mk.codingneko.com", "@cody@mk.codingneko.com",
"@kate@blahaj.zone",
"Interkosmos Link" "Interkosmos Link"
] ]
} }