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
branches:
include: [ main, develop, feature/* ]
include: [ main, beta, develop, feature/* ]

View File

@ -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", "--" ]

View File

@ -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)"

View File

@ -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(スプラッシュスクリーン)用のカスタムメッセージ"

View File

@ -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",

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,
})
public publickey: string;
@Column('boolean', {
default: false,
})
public sendReadMessage: boolean;
}

View File

@ -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.',

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) : [];
function nyaizeNode(node: mfm.MfmNode) {
if (node.type === "quote") return;

View File

@ -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(

View File

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

View File

@ -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

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___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],

View File

@ -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(
{

View File

@ -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) {

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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");

View File

@ -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,

View File

@ -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");

View File

@ -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");

View File

@ -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);

View File

@ -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")

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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;
});

View File

@ -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,

View File

@ -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,
};
});

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 = {
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,
});
});

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 bcrypt from "bcryptjs";
import * as speakeasy from "speakeasy";
import signin from "../common/signin.js";
import config from "@/config/index.js";

View File

@ -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(),

View File

@ -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();

View File

@ -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: {

View File

@ -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;

View File

@ -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 {

View File

@ -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);

View File

@ -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) > & {

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%;
margin-top: 1em;
position: sticky;
bottom: 1em;
bottom: var(--stickyBottom);
> span {
display: inline-block;

View File

@ -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';

View File

@ -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,
});
}

View File

@ -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(() => []);

View File

@ -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,
});
}

View File

@ -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

View File

@ -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;
}

View File

@ -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;

View File

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