Merge branch 'develop' into feature/capacitor
This commit is contained in:
commit
1f9e35f0cf
|
@ -18,4 +18,4 @@ services:
|
|||
image: redis
|
||||
|
||||
branches:
|
||||
include: [ main, develop, feature/* ]
|
||||
include: [ main, beta, develop, feature/* ]
|
||||
|
|
|
@ -3,7 +3,7 @@ FROM node:19-alpine as build
|
|||
WORKDIR /calckey
|
||||
|
||||
# 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 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/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/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
|
||||
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/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/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
|
||||
ENTRYPOINT [ "/sbin/tini", "--" ]
|
||||
|
|
|
@ -12,6 +12,7 @@ fetchingAsApObject: "Fetching from the Fediverse"
|
|||
ok: "OK"
|
||||
gotIt: "Got it!"
|
||||
cancel: "Cancel"
|
||||
noThankYou: "No thank you"
|
||||
enterUsername: "Enter username"
|
||||
renotedBy: "Boosted by {user}"
|
||||
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."
|
||||
flagAsCat: "Are you 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"
|
||||
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"
|
||||
|
@ -914,6 +917,13 @@ navbar: "Navigation bar"
|
|||
shuffle: "Shuffle"
|
||||
account: "Account"
|
||||
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"
|
||||
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."
|
||||
|
@ -1228,13 +1238,13 @@ _sfx:
|
|||
_ago:
|
||||
future: "Future"
|
||||
justNow: "Just now"
|
||||
secondsAgo: "{n} second(s) ago"
|
||||
minutesAgo: "{n} minute(s) ago"
|
||||
hoursAgo: "{n} hour(s) ago"
|
||||
daysAgo: "{n} day(s) ago"
|
||||
weeksAgo: "{n} week(s) ago"
|
||||
monthsAgo: "{n} month(s) ago"
|
||||
yearsAgo: "{n} year(s) ago"
|
||||
secondsAgo: "{n}s ago"
|
||||
minutesAgo: "{n}m ago"
|
||||
hoursAgo: "{n}h ago"
|
||||
daysAgo: "{n}d ago"
|
||||
weeksAgo: "{n}w ago"
|
||||
monthsAgo: "{n}mo ago"
|
||||
yearsAgo: "{n}y ago"
|
||||
_time:
|
||||
second: "Second(s)"
|
||||
minute: "Minute(s)"
|
||||
|
|
|
@ -12,6 +12,7 @@ fetchingAsApObject: "連合宇宙から取得中"
|
|||
ok: "OK"
|
||||
gotIt: "わかった!"
|
||||
cancel: "キャンセル"
|
||||
noThankYou: "やめておく"
|
||||
enterUsername: "ユーザー名を入力"
|
||||
renotedBy: "{user}がブースト"
|
||||
noNotes: "投稿はありません"
|
||||
|
@ -145,7 +146,9 @@ cacheRemoteFilesDescription: "この設定を無効にすると、リモート
|
|||
flagAsBot: "Botとして設定"
|
||||
flagAsBotDescription: "このアカウントがBotである場合は、この設定をオンにします。オンにすると、反応の連鎖を防ぐためのフラグとして他の開発者に役立ったり、Calckeyのシステム上での扱いがBotに合ったものになります。"
|
||||
flagAsCat: "あなたは…猫?😺"
|
||||
flagAsCatDescription: "このアカウントが猫であることを示す場合は、このフラグをオンにします。"
|
||||
flagAsCatDescription: "このアカウントが猫であることを示す猫モードを有効にするには、このフラグをオンにします。"
|
||||
flagSpeakAsCat: "猫語で話す"
|
||||
flagSpeakAsCatDescription: "猫モードが有効の場合にオンにすると、あなたの投稿の「な」を「にゃ」に変換します。"
|
||||
flagShowTimelineReplies: "タイムラインに投稿の返信を表示する"
|
||||
flagShowTimelineRepliesDescription: "オンにすると、タイムラインにユーザーの他の投稿への返信も表示されます。"
|
||||
autoAcceptFollowed: "フォローしているユーザーからのフォロー申請を自動承認"
|
||||
|
@ -916,6 +919,13 @@ navbar: "ナビゲーションバー"
|
|||
shuffle: "シャッフル"
|
||||
account: "アカウント"
|
||||
move: "移動"
|
||||
pushNotification: "プッシュ通知"
|
||||
subscribePushNotification: "プッシュ通知を有効化"
|
||||
unsubscribePushNotification: "プッシュ通知を停止する"
|
||||
pushNotificationAlreadySubscribed: "プッシュ通知は有効です"
|
||||
pushNotificationNotSupported: "ブラウザかサーバーがプッシュ通知に非対応"
|
||||
sendPushNotificationReadMessage: "通知やメッセージが既読になったらプッシュ通知を削除する"
|
||||
sendPushNotificationReadMessageCaption: "「{emptyPushNotificationMessage}」という通知が一瞬表示されるようになります。端末の電池消費量が増加する可能性があります。"
|
||||
adminCustomCssWarn: "この設定は、それが何をするものであるかを知っている場合のみ使用してください。不適切な値を入力すると、クライアントが正常に動作しなくなる可能性があります。ユーザー設定でCSSをテストし、正しく動作することを確認してください。"
|
||||
customMOTD: "カスタムMOTD(スプラッシュスクリーンメッセージ)"
|
||||
customMOTDDescription: "ユーザがページをロード/リロードするたびにランダムに表示される、改行で区切られたMOTD(スプラッシュスクリーン)用のカスタムメッセージ"
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "calckey",
|
||||
"version": "13.2.0-dev23",
|
||||
"version": "13.2.0-dev26",
|
||||
"codename": "aqua",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://codeberg.org/calckey/calckey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.1.0",
|
||||
"packageManager": "pnpm@8.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"rebuild": "pnpm run clean && pnpm -r run build && pnpm run gulp",
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
`);
|
||||
}
|
||||
}
|
|
@ -41,4 +41,9 @@ export class SwSubscription {
|
|||
length: 128,
|
||||
})
|
||||
public publickey: string;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public sendReadMessage: boolean;
|
||||
}
|
||||
|
|
|
@ -156,6 +156,12 @@ export class User {
|
|||
})
|
||||
public isCat: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: true,
|
||||
comment: 'Whether to speak as a cat if isCat.',
|
||||
})
|
||||
public speakAsCat: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
comment: 'Whether the User is the admin.',
|
||||
|
|
|
@ -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) : [];
|
||||
function nyaizeNode(node: mfm.MfmNode) {
|
||||
if (node.type === "quote") return;
|
||||
|
|
|
@ -438,6 +438,7 @@ export const UserRepository = db.getRepository(User).extend({
|
|||
isModerator: user.isModerator || falsy,
|
||||
isBot: user.isBot || falsy,
|
||||
isCat: user.isCat || falsy,
|
||||
speakAsCat: user.speakAsCat || falsy,
|
||||
instance: user.host
|
||||
? userInstanceCache
|
||||
.fetch(
|
||||
|
|
|
@ -66,6 +66,11 @@ export const packedUserLiteSchema = {
|
|||
nullable: false,
|
||||
optional: true,
|
||||
},
|
||||
speakAsCat: {
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
optional: true,
|
||||
},
|
||||
emojis: {
|
||||
type: "array",
|
||||
nullable: false,
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { generateKeyPair } from "node:crypto";
|
||||
import generateUserToken from "./generate-native-user-token.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 { db } from "@/db/postgre.js";
|
||||
import config from "@/config/index.js";
|
||||
import { hashPassword } from "@/misc/password.js";
|
||||
|
||||
export async function signup(opts: {
|
||||
username: User["username"];
|
||||
|
@ -42,8 +42,7 @@ export async function signup(opts: {
|
|||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
hash = await bcrypt.hash(password, salt);
|
||||
hash = await hashPassword(password);
|
||||
}
|
||||
|
||||
// Generate secret
|
||||
|
|
|
@ -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___serverInfo from "./endpoints/server-info.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_unregister from "./endpoints/sw/unregister.js";
|
||||
import * as ep___test from "./endpoints/test.js";
|
||||
|
@ -637,6 +639,8 @@ const eps = [
|
|||
["stats", ep___stats],
|
||||
["sw/register", ep___sw_register],
|
||||
["sw/unregister", ep___sw_unregister],
|
||||
['sw/show-registration', ep___sw_show_registration],
|
||||
['sw/update-registration', ep___sw_update_registration],
|
||||
["test", ep___test],
|
||||
["username/available", ep___username_available],
|
||||
["users", ep___users],
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import define from "../../define.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
// import bcrypt from "bcryptjs";
|
||||
import rndstr from "rndstr";
|
||||
import { Users, UserProfiles } from "@/models/index.js";
|
||||
import { hashPassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["admin"],
|
||||
|
@ -47,7 +48,8 @@ export default define(meta, paramDef, async (ps) => {
|
|||
const passwd = rndstr("a-zA-Z0-9", 8);
|
||||
|
||||
// Generate hash of password
|
||||
const hash = bcrypt.hashSync(passwd);
|
||||
// const hash = bcrypt.hashSync(passwd);
|
||||
const hash = await hashPassword(passwd);
|
||||
|
||||
await UserProfiles.update(
|
||||
{
|
||||
|
|
|
@ -209,7 +209,12 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { promisify } from "node:util";
|
||||
import * as cbor from "cbor";
|
||||
import define from "../../../define.js";
|
||||
|
@ -11,6 +10,7 @@ import {
|
|||
import config from "@/config/index.js";
|
||||
import { procedures, hash } from "../../../2fa.js";
|
||||
import { publishMainStream } from "@/services/stream.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
|
||||
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 });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import define from "../../../define.js";
|
||||
import { UserProfiles, AttestationChallenges } from "@/models/index.js";
|
||||
import { promisify } from "node:util";
|
||||
import * as crypto from "node:crypto";
|
||||
import { genId } from "@/misc/gen-id.js";
|
||||
import { hash } from "../../../2fa.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
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 });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import * as speakeasy from "speakeasy";
|
||||
import * as QRCode from "qrcode";
|
||||
import config from "@/config/index.js";
|
||||
import { UserProfiles } from "@/models/index.js";
|
||||
import define from "../../../define.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -23,7 +23,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
import define from "../../../define.js";
|
||||
import { UserProfiles, UserSecurityKeys, Users } from "@/models/index.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 });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import define from "../../../define.js";
|
||||
import { UserProfiles } from "@/models/index.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -20,7 +20,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import define from "../../define.js";
|
||||
import { UserProfiles } from "@/models/index.js";
|
||||
import { hashPassword, comparePassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -21,15 +21,14 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.currentPassword, profile.password!);
|
||||
const same = await comparePassword(ps.currentPassword, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(ps.newPassword, salt);
|
||||
const hash = await hashPassword(ps.newPassword);
|
||||
|
||||
await UserProfiles.update(user.id, {
|
||||
password: hash,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { UserProfiles, Users } from "@/models/index.js";
|
||||
import { deleteAccount } from "@/services/delete-account.js";
|
||||
import define from "../../define.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -25,7 +25,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import {
|
||||
publishInternalEvent,
|
||||
publishMainStream,
|
||||
|
@ -7,6 +6,7 @@ import {
|
|||
import generateUserToken from "../../common/generate-native-user-token.js";
|
||||
import define from "../../define.js";
|
||||
import { Users, UserProfiles } from "@/models/index.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -29,7 +29,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error("incorrect password");
|
||||
|
|
|
@ -2,12 +2,12 @@ import { publishMainStream } from "@/services/stream.js";
|
|||
import define from "../../define.js";
|
||||
import rndstr from "rndstr";
|
||||
import config from "@/config/index.js";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { Users, UserProfiles } from "@/models/index.js";
|
||||
import { sendEmail } from "@/services/send-email.js";
|
||||
import { ApiError } from "../../error.js";
|
||||
import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
|
||||
import { HOUR } from "@/const.js";
|
||||
import { comparePassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -47,7 +47,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
const same = await comparePassword(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new ApiError(meta.errors.incorrectPassword);
|
||||
|
|
|
@ -104,6 +104,7 @@ export const paramDef = {
|
|||
noCrawle: { type: "boolean" },
|
||||
isBot: { type: "boolean" },
|
||||
isCat: { type: "boolean" },
|
||||
speakAsCat: { type: "boolean" },
|
||||
showTimelineReplies: { type: "boolean" },
|
||||
injectFeaturedNote: { type: "boolean" },
|
||||
receiveAnnouncementEmail: { type: "boolean" },
|
||||
|
@ -191,6 +192,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
|||
profileUpdates.autoAcceptFollowed = ps.autoAcceptFollowed;
|
||||
if (typeof ps.noCrawle === "boolean") profileUpdates.noCrawle = ps.noCrawle;
|
||||
if (typeof ps.isCat === "boolean") updates.isCat = ps.isCat;
|
||||
if (typeof ps.speakAsCat === "boolean") updates.speakAsCat = ps.speakAsCat;
|
||||
if (typeof ps.injectFeaturedNote === "boolean")
|
||||
profileUpdates.injectFeaturedNote = ps.injectFeaturedNote;
|
||||
if (typeof ps.receiveAnnouncementEmail === "boolean")
|
||||
|
|
|
@ -93,13 +93,27 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (user) {
|
||||
activeUsersChart.read(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;
|
||||
}
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
if (found.length > ps.limit) {
|
||||
found.length = ps.limit;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
|
|
@ -151,11 +151,25 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -123,13 +123,27 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (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;
|
||||
});
|
||||
|
|
|
@ -86,9 +86,24 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
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;
|
||||
});
|
||||
|
|
|
@ -126,13 +126,27 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
if (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;
|
||||
});
|
||||
|
|
|
@ -74,7 +74,21 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (user) generateMutedUserQuery(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;
|
||||
});
|
||||
|
|
|
@ -58,7 +58,21 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
if (user) generateMutedUserQuery(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;
|
||||
});
|
||||
|
|
|
@ -145,8 +145,21 @@ export default define(meta, paramDef, async (ps, me) => {
|
|||
}
|
||||
}
|
||||
|
||||
// Search notes
|
||||
const notes = 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, 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;
|
||||
});
|
||||
|
|
|
@ -143,11 +143,25 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
|
||||
process.nextTick(() => {
|
||||
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;
|
||||
});
|
||||
|
|
|
@ -138,9 +138,27 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
//#endregion
|
||||
|
||||
const timeline = await query.take(ps.limit).getMany();
|
||||
process.nextTick(() => {
|
||||
if (user) {
|
||||
activeUsersChart.read(user);
|
||||
}
|
||||
});
|
||||
|
||||
activeUsersChart.read(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;
|
||||
}
|
||||
|
||||
return await Notes.packMany(timeline, user);
|
||||
if (found.length > ps.limit) {
|
||||
found.length = ps.limit;
|
||||
}
|
||||
|
||||
return found;
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { publishMainStream } from "@/services/stream.js";
|
||||
import { Users, UserProfiles, PasswordResetRequests } from "@/models/index.js";
|
||||
import define from "../define.js";
|
||||
import { ApiError } from "../error.js";
|
||||
import { hashPassword } from "@/misc/password.js";
|
||||
|
||||
export const meta = {
|
||||
tags: ["reset password"],
|
||||
|
@ -34,8 +34,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
}
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(ps.password, salt);
|
||||
const hash = await hashPassword(ps.password);
|
||||
|
||||
await UserProfiles.update(req.userId, {
|
||||
password: hash,
|
||||
|
|
|
@ -26,6 +26,18 @@ export const meta = {
|
|||
optional: false,
|
||||
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;
|
||||
|
@ -36,14 +48,15 @@ export const paramDef = {
|
|||
endpoint: { type: "string" },
|
||||
auth: { type: "string" },
|
||||
publickey: { type: "string" },
|
||||
sendReadMessage: { type: 'boolean', default: false },
|
||||
},
|
||||
required: ["endpoint", "auth", "publickey"],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
// if already subscribed
|
||||
const exist = await SwSubscriptions.findOneBy({
|
||||
userId: user.id,
|
||||
userId: me.id,
|
||||
endpoint: ps.endpoint,
|
||||
auth: ps.auth,
|
||||
publickey: ps.publickey,
|
||||
|
@ -55,20 +68,27 @@ export default define(meta, paramDef, async (ps, user) => {
|
|||
return {
|
||||
state: "already-subscribed" as const,
|
||||
key: instance.swPublicKey,
|
||||
userId: me.id,
|
||||
endpoint: exist.endpoint,
|
||||
sendReadMessage: exist.sendReadMessage,
|
||||
};
|
||||
}
|
||||
|
||||
await SwSubscriptions.insert({
|
||||
id: genId(),
|
||||
createdAt: new Date(),
|
||||
userId: user.id,
|
||||
userId: me.id,
|
||||
endpoint: ps.endpoint,
|
||||
auth: ps.auth,
|
||||
publickey: ps.publickey,
|
||||
sendReadMessage: ps.sendReadMessage,
|
||||
});
|
||||
|
||||
return {
|
||||
state: "subscribed" as const,
|
||||
key: instance.swPublicKey,
|
||||
userId: me.id,
|
||||
endpoint: ps.endpoint,
|
||||
sendReadMessage: ps.sendReadMessage,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
|
@ -4,7 +4,7 @@ import define from "../../define.js";
|
|||
export const meta = {
|
||||
tags: ["account"],
|
||||
|
||||
requireCredential: true,
|
||||
requireCredential: false,
|
||||
|
||||
description: "Unregister from receiving push notifications.",
|
||||
} as const;
|
||||
|
@ -17,9 +17,9 @@ export const paramDef = {
|
|||
required: ["endpoint"],
|
||||
} as const;
|
||||
|
||||
export default define(meta, paramDef, async (ps, user) => {
|
||||
export default define(meta, paramDef, async (ps, me) => {
|
||||
await SwSubscriptions.delete({
|
||||
userId: user.id,
|
||||
...(me ? { userId: me.id } : {}),
|
||||
endpoint: ps.endpoint,
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
});
|
|
@ -1,5 +1,4 @@
|
|||
import type Koa from "koa";
|
||||
import bcrypt from "bcryptjs";
|
||||
import * as speakeasy from "speakeasy";
|
||||
import signin from "../common/signin.js";
|
||||
import config from "@/config/index.js";
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type Koa from "koa";
|
||||
import rndstr from "rndstr";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { fetchMeta } from "@/misc/fetch-meta.js";
|
||||
import { verifyHcaptcha, verifyRecaptcha } from "@/misc/captcha.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 { genId } from "@/misc/gen-id.js";
|
||||
import { validateEmailForAccount } from "@/services/validate-email-for-account.js";
|
||||
import { hashPassword } from "@/misc/password.js";
|
||||
|
||||
export default async (ctx: Koa.Context) => {
|
||||
const body = ctx.request.body;
|
||||
|
@ -79,8 +79,7 @@ export default async (ctx: Koa.Context) => {
|
|||
const code = rndstr("a-z0-9", 16);
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
await UserPendings.insert({
|
||||
id: genId(),
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import bcrypt from "bcryptjs";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import generateNativeUserToken from "../server/api/common/generate-native-user-token.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 { UsedUsername } from "@/models/entities/used-username.js";
|
||||
import { db } from "@/db/postgre.js";
|
||||
import { hashPassword } from "@/misc/password.js";
|
||||
|
||||
export async function createSystemUser(username: string) {
|
||||
const password = uuid();
|
||||
|
||||
// Generate hash of password
|
||||
const salt = await bcrypt.genSalt(8);
|
||||
const hash = await bcrypt.hash(password, salt);
|
||||
const hash = await hashPassword(password);
|
||||
|
||||
// Generate secret
|
||||
const secret = generateNativeUserToken();
|
||||
|
|
|
@ -63,6 +63,13 @@ export async function pushNotification<T extends keyof pushNotificationsTypes>(
|
|||
});
|
||||
|
||||
for (const subscription of subscriptions) {
|
||||
if ([
|
||||
'readNotifications',
|
||||
'readAllNotifications',
|
||||
'readAllMessagingMessages',
|
||||
'readAllMessagingMessagesOfARoom',
|
||||
].includes(type) && !subscription.sendReadMessage) continue;
|
||||
|
||||
const pushSubscription = {
|
||||
endpoint: subscription.endpoint,
|
||||
keys: {
|
||||
|
|
|
@ -141,7 +141,7 @@ onBeforeUnmount(() => {
|
|||
<style lang="scss" scoped>
|
||||
.kpoogebi {
|
||||
position: relative;
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
|
|
|
@ -10,16 +10,16 @@
|
|||
:class="{ renote: isRenote }"
|
||||
>
|
||||
<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 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="pinned" class="info"><i class="ph-push-pin ph-bold ph-lg"></i>{{ i18n.ts.pinnedNote }}</div>
|
||||
<div v-if="isRenote" class="renote">
|
||||
<i class="ph-repeat ph-bold ph-lg"></i>
|
||||
<I18n :src="i18n.ts.renotedBy" tag="span">
|
||||
<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"/>
|
||||
</MkA>
|
||||
</template>
|
||||
|
@ -93,7 +93,6 @@
|
|||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</footer>
|
||||
<!-- <MkNoteFooter :note="appearNote"></MkNoteFooter> -->
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
@ -418,8 +417,8 @@ function readPromo() {
|
|||
align-items: center;
|
||||
white-space: pre;
|
||||
color: var(--renote);
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
> i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
@ -504,7 +503,7 @@ function readPromo() {
|
|||
width: 100%;
|
||||
margin-top: 1em;
|
||||
position: sticky;
|
||||
bottom: 1em;
|
||||
bottom: var(--stickyBottom);
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
|
@ -663,6 +662,9 @@ function readPromo() {
|
|||
}
|
||||
> .line {
|
||||
margin-right: 10px;
|
||||
&::before {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .article {
|
||||
|
|
|
@ -352,6 +352,7 @@ onUnmounted(() => {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.lxwezrsl {
|
||||
font-size: 1.05em;
|
||||
position: relative;
|
||||
transition: box-shadow 0.1s ease;
|
||||
contain: content;
|
||||
|
@ -451,7 +452,7 @@ onUnmounted(() => {
|
|||
&:last-child {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
font-size: 1.2em;
|
||||
font-size: 1.1em;
|
||||
overflow: clip;
|
||||
outline: none;
|
||||
scroll-margin-top: calc(var(--stickyTop) + 20vh);
|
||||
|
|
|
@ -7,25 +7,25 @@
|
|||
<div v-if="conversation && depth > 1" class="line"></div>
|
||||
<div class="main" @click="noteClick">
|
||||
<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>
|
||||
<div class="body">
|
||||
<XNoteHeader class="header" :note="note" :mini="true"/>
|
||||
<div class="body">
|
||||
<p v-if="note.cw != null" class="cw">
|
||||
<MkA v-if="note.replyId" :to="`/notes/${note.replyId}`" class="reply-icon" @click.stop>
|
||||
<p v-if="appearNote.cw != null" class="cw">
|
||||
<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>
|
||||
</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>
|
||||
</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/>
|
||||
<XCwButton v-model="showContent" :note="note"/>
|
||||
</p>
|
||||
<div v-show="note.cw == null || showContent" class="content">
|
||||
<MkSubNoteContent class="text" :note="note" :detailed="true" :parentId="note.parentId" :conversation="conversation"/>
|
||||
<div v-show="appearNote.cw == null || showContent" class="content">
|
||||
<MkSubNoteContent class="text" :note="note" :detailed="true" :parentId="appearNote.parentId" :conversation="conversation"/>
|
||||
</div>
|
||||
<div v-if="translating || translation" class="translation">
|
||||
<MkLoading v-if="translating" mini/>
|
||||
|
@ -56,15 +56,14 @@
|
|||
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
|
||||
</button>
|
||||
</footer>
|
||||
<!-- <MkNoteFooter :note="note" :directReplies="replies.length"></MkNoteFooter> -->
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="conversation">
|
||||
<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 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>
|
||||
<div v-else-if="replies.length > 0" class="more">
|
||||
<div class="line"></div>
|
||||
|
@ -457,6 +456,26 @@ function noteClick(e) {
|
|||
-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 {
|
||||
:not(.reply) > & {
|
||||
|
|
|
@ -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>
|
|
@ -130,7 +130,7 @@ const urls = props.note.text ? extractUrlFromMfm(mfm.parse(props.note.text)) : n
|
|||
width: 100%;
|
||||
margin-top: 1em;
|
||||
position: sticky;
|
||||
bottom: 1em;
|
||||
bottom: var(--stickyBottom);
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
<MkSparkle>
|
||||
<h3>{{ i18n.ts._tutorial.step6_4 }} <Mfm text="$[shake 🚀]"></Mfm></h3>
|
||||
</MkSparkle>
|
||||
<MkPushNotificationAllowButton primary show-only-to-register/>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
@ -122,6 +123,7 @@ import MkButton from '@/components/MkButton.vue';
|
|||
import XFeaturedUsers from '@/pages/explore.users.vue';
|
||||
import XPostForm from '@/components/MkPostForm.vue';
|
||||
import MkSparkle from '@/components/MkSparkle.vue';
|
||||
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
|
||||
import { defaultStore } from '@/store';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
|
|
|
@ -35,8 +35,10 @@
|
|||
|
||||
<FormSection v-if="iAmModerator">
|
||||
<template #label>Moderation</template>
|
||||
<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>
|
||||
<FormSuspense :p="init">
|
||||
<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>
|
||||
</FormSuspense>
|
||||
<MkButton @click="refreshMetadata"><i class="ph-arrows-clockwise ph-bold ph-lg"></i> Refresh metadata</MkButton>
|
||||
</FormSection>
|
||||
|
||||
|
@ -158,6 +160,13 @@ import 'swiper/scss';
|
|||
import 'swiper/scss/virtual';
|
||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy';
|
||||
|
||||
type AugmentedInstanceMetadata = misskey.entities.DetailedInstanceMetadata & {
|
||||
blockedHosts: string[];
|
||||
};
|
||||
type AugmentedInstance = misskey.entities.Instance & {
|
||||
isBlocked: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
host: string;
|
||||
}>();
|
||||
|
@ -168,8 +177,8 @@ let tab = $ref(tabs[0]);
|
|||
watch($$(tab), () => (syncSlide(tabs.indexOf(tab))));
|
||||
|
||||
let chartSrc = $ref('instance-requests');
|
||||
let meta = $ref<misskey.entities.DetailedInstanceMetadata | null>(null);
|
||||
let instance = $ref<misskey.entities.Instance | null>(null);
|
||||
let meta = $ref<AugmentedInstanceMetadata | null>(null);
|
||||
let instance = $ref<AugmentedInstance | null>(null);
|
||||
let suspended = $ref(false);
|
||||
let isBlocked = $ref(false);
|
||||
let faviconUrl = $ref(null);
|
||||
|
@ -185,19 +194,34 @@ const usersPagination = {
|
|||
offsetMode: true,
|
||||
};
|
||||
|
||||
async function fetch() {
|
||||
instance = await os.api('federation/show-instance', {
|
||||
host: props.host,
|
||||
});
|
||||
suspended = instance.isSuspended;
|
||||
isBlocked = instance.isBlocked;
|
||||
faviconUrl = getProxiedImageUrlNullable(instance.faviconUrl, 'preview') ?? getProxiedImageUrlNullable(instance.iconUrl, 'preview');
|
||||
async function init() {
|
||||
meta = await os.api('admin/meta');
|
||||
}
|
||||
|
||||
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 (!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', {
|
||||
blockedHosts: isBlocked ? meta.blockedHosts.concat([instance.host]) : meta.blockedHosts.filter(x => x !== instance.host),
|
||||
blockedHosts,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,21 @@
|
|||
<FormButton class="_formBlock" @click="readAllUnreadNotes">{{ i18n.ts.markAsReadAllUnreadNotes }}</FormButton>
|
||||
<FormButton class="_formBlock" @click="readAllMessagingMessages">{{ i18n.ts.markAsReadAllTalkMessages }}</FormButton>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
@ -19,6 +34,11 @@ import * as os from '@/os';
|
|||
import { $i } from '@/account';
|
||||
import { i18n } from '@/i18n';
|
||||
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() {
|
||||
await os.api('i/read-all-unread-notes');
|
||||
|
@ -49,6 +69,18 @@ function configure() {
|
|||
}, '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 headerTabs = $computed(() => []);
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
</FormSlot>
|
||||
|
||||
<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.isBot" class="_formBlock">{{ i18n.ts.flagAsBot }}<template #caption>{{ i18n.ts.flagAsBotDescription }}</template></FormSwitch>
|
||||
<div v-if="saveButton == true">
|
||||
|
@ -92,6 +93,7 @@ const profile = reactive({
|
|||
lang: $i?.lang,
|
||||
isBot: $i?.isBot,
|
||||
isCat: $i?.isCat,
|
||||
speakAsCat: $i?.speakAsCat,
|
||||
showTimelineReplies: $i?.showTimelineReplies,
|
||||
});
|
||||
|
||||
|
@ -135,6 +137,7 @@ function save() {
|
|||
lang: profile.lang || null,
|
||||
isBot: !!profile.isBot,
|
||||
isCat: !!profile.isCat,
|
||||
speakAsCat: !!profile.speakAsCat,
|
||||
showTimelineReplies: !!profile.showTimelineReplies,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -339,7 +339,7 @@ function syncSlide(index) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
syncSlide(timelines.indexOf(swiperRef.activeIndex));
|
||||
syncSlide(timelines.indexOf(defaultStore.state.tl?.src || swiperRef.activeIndex));
|
||||
});
|
||||
|
||||
// #v-ifdef VITE_CAPACITOR
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
import { instance } from "@/instance";
|
||||
import { $i } from "@/account";
|
||||
import { api } from "@/os";
|
||||
import { lang } from "@/config";
|
||||
|
||||
export async function initializeSw() {
|
||||
|
@ -12,58 +9,5 @@ export async function initializeSw() {
|
|||
msg: "initialize",
|
||||
lang,
|
||||
});
|
||||
|
||||
if (instance.swPublickey && "PushManager" in window && $i && $i.token) {
|
||||
// SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters
|
||||
registration.pushManager
|
||||
.subscribe({
|
||||
userVisibleOnly: true,
|
||||
applicationServerKey: urlBase64ToUint8Array(instance.swPublickey),
|
||||
})
|
||||
.then((subscription) => {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="dkgtipfy" :class="{ wallpaper }">
|
||||
<div class="dkgtipfy" :class="{ wallpaper, isMobile }">
|
||||
<XSidebar v-if="!isMobile" class="sidebar"/>
|
||||
|
||||
<MkStickyContainer class="contents">
|
||||
|
@ -319,6 +319,10 @@ console.log(mainRouter.currentRoute.value.name);
|
|||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
--stickyBottom: 1em;
|
||||
&.isMobile {
|
||||
--stickyBottom: 6rem;
|
||||
}
|
||||
&.wallpaper {
|
||||
background: var(--wallpaperOverlay);
|
||||
//backdrop-filter: var(--blur, blur(4px));
|
||||
|
@ -363,7 +367,7 @@ console.log(mainRouter.currentRoute.value.name);
|
|||
}
|
||||
|
||||
> .postButton, .widgetButton {
|
||||
bottom: 6rem;
|
||||
bottom: var(--stickyBottom);
|
||||
right: 1.5rem;
|
||||
height: 4rem;
|
||||
width: 4rem;
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"@trapezial@calckey.jp",
|
||||
"@unattributed@calckey.social",
|
||||
"@cody@mk.codingneko.com",
|
||||
"@kate@blahaj.zone",
|
||||
"Interkosmos Link"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue