Compare commits

...

33 Commits

Author SHA1 Message Date
naskya d51bfde68a Merge branch 'redis' into 'develop'
Make Redis accessible from backend-rs


See merge request firefish/firefish!10753
2024-04-21 22:23:16 +00:00
naskya a6498b0491 Merge branch 'feat/alt_warning' into 'develop'
feat(client): attachment without description opened when choose add alt

Co-authored-by: 老周部落 <laozhoubuluo@gmail.com>

See merge request firefish/firefish!10699
2024-04-21 22:20:45 +00:00
naskya 9ced0d96ad
chore (client): don't make a new post as soon as you add descriptions 2024-04-22 07:20:07 +09:00
naskya 9a4988eaad
chore (client): fix type errors 2024-04-22 07:14:56 +09:00
naskya fbdc068115
locale: update en-US.yml 2024-04-22 07:09:54 +09:00
naskya 23ec206aee
Merge branch 'develop' into feat/alt_warning 2024-04-22 06:59:58 +09:00
naskya 07444ae7c1
Merge branch 'develop' into redis 2024-04-22 06:42:53 +09:00
naskya 455ecdf743 Merge branch 'docs/add_dependencies' into 'develop'
docs: add minimum dependencies

Co-authored-by: 老周部落 <laozhoubuluo@gmail.com>

See merge request firefish/firefish!10703
2024-04-21 21:40:34 +00:00
naskya d98c564ead
docs: move the dependencies section to the top 2024-04-22 06:39:20 +09:00
naskya 56aac15a6b
docs (minor): paraphrase a bit 2024-04-22 06:32:07 +09:00
naskya 280dddf464 Merge branch 'fix/download-url-agent' into 'develop'
fix: download-url should use proxy bypass hosts

Co-authored-by: 老周部落 <laozhoubuluo@gmail.com>

See merge request firefish/firefish!10739
2024-04-21 21:27:55 +00:00
naskya 1347c6ff04
Merge branch 'develop' into redis 2024-04-22 06:23:17 +09:00
naskya b3cc01c440 Merge branch 'feat/show-MkRemoteCaution-in-history' into 'develop'
feat: show MkRemoteCaution in note history page

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10743
2024-04-21 21:23:02 +00:00
naskya ebaefb9697
chore (minor, client): remove redundant attribute 2024-04-22 06:02:08 +09:00
naskya d9982a0b6a
Merge branch 'develop' into feat/show-MkRemoteCaution-in-history 2024-04-22 06:01:19 +09:00
naskya 0cb2e94d99 Merge branch 'fix/feed' into 'develop'
fix: notes in rss feed do not display HTML

Co-authored-by: Lhcfl <Lhcfl@outlook.com>

See merge request firefish/firefish!10744
2024-04-21 21:00:18 +00:00
naskya d1817d9a22 Merge branch 'feat/antenna_limit' into 'develop'
feat: antenna limit

Co-authored-by: 老周部落 <laozhoubuluo@gmail.com>

Closes #10894

See merge request firefish/firefish!10740
2024-04-21 20:58:09 +00:00
naskya c9de5f6095
docs: update api-changes.md 2024-04-22 05:56:46 +09:00
naskya c4658801aa
chore: regenerate entities 2024-04-22 05:54:32 +09:00
naskya a107d8c1ec
fix (backend): update import 2024-04-22 05:52:56 +09:00
naskya 4c91e8e37f
Merge branch 'develop' into feat/antenna_limit 2024-04-22 05:51:22 +09:00
naskya 0c9dc92f07
fix (backend-rs): use hostname for Redis key prefix and host for Redis streams
I suspect this is a bug, but I keep this behavior for compatibility for now
2024-04-22 05:16:14 +09:00
naskya ce672f4edd
dev: add cargo test to pnpm scripts
mocha test has been unmaintained for a long time and is very broken :(
2024-04-21 22:36:05 +09:00
Lhcfl 241c824ab5 fix: use better `]]>` replacer 2024-04-14 16:44:12 +08:00
Lhcfl 54d9916fec fix: rss feed no HTML 2024-04-14 16:34:33 +08:00
Lhcfl f0a50bc288 feat: show MkRemoteCaution in note history page 2024-04-14 13:59:27 +08:00
老周部落 5eff4da27b
feat: antenna limit 2024-04-13 10:02:32 +08:00
老周部落 f44a2937d4
fix: download-url should use proxy bypass hosts 2024-04-13 09:00:08 +08:00
老周部落 46bf02cdd5
chore: format 2024-03-31 06:44:00 +08:00
老周部落 f346d2e2f6
feat: add showAddFileDescriptionAtFirstPost and allow cancel adding alt-text midway 2024-03-31 06:42:33 +08:00
naskya 2267e90d3b
refactor (client): await asynchronous processes, remove duplicate code 2024-03-31 06:41:20 +08:00
老周部落 db0bd21edc
feat(client): attachment without description opened when choose add alt 2024-03-31 06:41:07 +08:00
老周部落 de4da3c1fd
docs: add minimum dependencies 2024-03-19 22:51:05 +08:00
25 changed files with 285 additions and 61 deletions

View File

@ -42,6 +42,8 @@ cargo --version
### PostgreSQL and PGroonga
Firefish requires PostgreSQL v12 or later. We recommend that you install v12.x for the same reason as Node.js.
PostgreSQL install instructions can be found at [this page](https://www.postgresql.org/download/).
```sh

View File

@ -2,6 +2,10 @@
Breaking changes are indicated by the :warning: icon.
## Unreleased
- Added `antennaLimit` field to the response of `meta` and `admin/meta`, and the request of `admin/update-meta` (optional).
## v20240413
- :warning: Removed `patrons` endpoint.

View File

@ -1,9 +1,36 @@
# Install Firefish
This document shows an example procedure for installing Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
Firefish depends on the following software.
## Runtime dependencies
- At least [NodeJS](https://nodejs.org/en/) v18.17.0 (v20/v21 recommended)
- At least [PostgreSQL](https://www.postgresql.org/) v12 (v16 recommended) with [PGroonga](https://pgroonga.github.io/) extension
- At least [Redis](https://redis.io/) v7
- Web Proxy (one of the following)
- Caddy (recommended)
- Nginx (recommended)
- Apache
- [FFmpeg](https://ffmpeg.org/) for video transcoding (**optional**)
- Caching server (**optional**, one of the following)
- [DragonflyDB](https://www.dragonflydb.io/)
- [KeyDB](https://keydb.dev/)
- Another [Redis](https://redis.io/) server
## Build dependencies
- At least [Rust](https://www.rust-lang.org/) v1.74
- C/C++ compiler & build tools
- `build-essential` on Debian/Ubuntu Linux
- `base-devel` on Arch Linux
- [Python 3](https://www.python.org/)
This document shows an example procedure for installing these dependencies and Firefish on Debian 12. Note that there is much room for customizing the server setup; this document merely demonstrates a simple installation.
If you want to use the pre-built container image, please refer to [`install-container.md`](./install-container.md).
If you do not prepare your environment as document, be sure to meet the minimum dependencies given at the bottom of the page.
Make sure that you can use the `sudo` command before proceeding.
## 1. Install dependencies

View File

@ -394,6 +394,7 @@ enableRegistration: "Enable new user registration"
invite: "Invite"
driveCapacityPerLocalAccount: "Drive capacity per local user"
driveCapacityPerRemoteAccount: "Drive capacity per remote user"
antennaLimit: "The maximum number of antennas that each user can create"
inMb: "In megabytes"
iconUrl: "Icon URL"
bannerUrl: "Banner image URL"
@ -1226,6 +1227,8 @@ publishTimelinesDescription: "If enabled, the Local and Global timelines will be
on {url} even when signed out."
noAltTextWarning: "Some attached file(s) have no description. Did you forget to write?"
showNoAltTextWarning: "Show a warning if you attempt to post files without a description"
showAddFileDescriptionAtFirstPost: "Automatically open a form to write a description when you
attempt to post files without a description"
_emojiModPerm:
unauthorized: "None"

View File

@ -340,6 +340,7 @@ invite: "邀请"
driveCapacityPerLocalAccount: "每个本地用户的网盘容量"
driveCapacityPerRemoteAccount: "每个远程用户的网盘容量"
inMb: "以兆字节 (MegaByte) 为单位"
antennaLimit: "每个用户最多可以创建的天线数量"
iconUrl: "图标 URL"
bannerUrl: "横幅图 URL"
backgroundImageUrl: "背景图 URL"
@ -2053,6 +2054,7 @@ searchRangeDescription: "如果您要过滤时间段,请按以下格式输入
messagingUnencryptedInfo: "Firefish 上的聊天没有经过端到端加密,请不要在聊天中分享您的敏感信息。"
noAltTextWarning: 有些附件没有描述。您是否忘记写描述了?
showNoAltTextWarning: 当您尝试发布没有描述的帖子附件时显示警告
showAddFileDescriptionAtFirstPost: 当您首次尝试发布没有描述的帖子附件时自动弹出添加描述页面
autocorrectNoteLanguage: 当帖子语言不符合自动检测的结果的时候显示警告
incorrectLanguageWarning: "看上去您帖子使用的语言是{detected},但您选择的语言是{current}。\n要改为以{detected}发帖吗?"
noteEditHistory: "帖子编辑历史"

View File

@ -26,7 +26,9 @@
"debug": "pnpm run build:debug && pnpm run start",
"build:debug": "pnpm run clean && pnpm node ./scripts/dev-build.mjs && pnpm run gulp",
"mocha": "pnpm --filter backend run mocha",
"test": "pnpm run mocha",
"test": "pnpm run test:ts && pnpm run test:rs",
"test:ts": "pnpm run mocha",
"test:rs": "cargo test",
"format": "pnpm run format:ts; pnpm run format:rs",
"format:ts": "pnpm -r --parallel run format",
"format:rs": "cargo fmt --all --",

View File

@ -557,6 +557,7 @@ export interface Meta {
recaptchaSecretKey: string | null
localDriveCapacityMb: number
remoteDriveCapacityMb: number
antennaLimit: number
summalyProxy: string | null
enableEmail: boolean
email: string | null

View File

@ -320,7 +320,7 @@ fn load_config() -> Config {
} else {
server_config.redis.prefix.clone()
}
.unwrap_or(host.clone());
.unwrap_or(hostname.clone());
Config {
url: server_config.url,

View File

@ -174,6 +174,8 @@ pub struct Model {
pub more_urls: Json,
#[sea_orm(column_name = "markLocalFilesNsfwByDefault")]
pub mark_local_files_nsfw_by_default: bool,
#[sea_orm(column_name = "antennaLimit")]
pub antenna_limit: i32,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]

View File

@ -0,0 +1,19 @@
import type { MigrationInterface, QueryRunner } from "typeorm";
export class antennaLimit1712937600000 implements MigrationInterface {
async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "meta" ADD "antennaLimit" integer NOT NULL DEFAULT 5`,
undefined,
);
await queryRunner.query(
`COMMENT ON COLUMN "meta"."antennaLimit" IS 'Antenna Limit'`,
);
}
async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "meta" DROP COLUMN "antennaLimit"`,
undefined,
);
}
}

View File

@ -2,8 +2,8 @@ import * as fs from "node:fs";
import * as stream from "node:stream";
import * as util from "node:util";
import got, * as Got from "got";
import { httpAgent, httpsAgent, StatusError } from "./fetch.js";
import { config } from "@/config.js";
import { getAgentByHostname, StatusError } from "./fetch.js";
import chalk from "chalk";
import Logger from "@/services/logger.js";
import IPCIDR from "ip-cidr";
@ -40,10 +40,7 @@ export async function downloadUrl(url: string, path: string): Promise<void> {
send: timeout,
request: operationTimeout, // whole operation timeout
},
agent: {
http: httpAgent,
https: httpsAgent,
},
agent: getAgentByHostname(new URL(url).hostname),
http2: false, // default
retry: {
limit: 0,

View File

@ -171,6 +171,25 @@ export function getAgentByUrl(url: URL, bypassProxy = false) {
}
}
/**
* Get agent by Hostname
* @param hostname Hostname
* @param bypassProxy Allways bypass proxy
*/
export function getAgentByHostname(hostname: string, bypassProxy = false) {
if (bypassProxy || (config.proxyBypassHosts || []).includes(hostname)) {
return {
http: _http,
https: _https,
};
} else {
return {
http: httpAgent,
https: httpsAgent,
};
}
}
export class StatusError extends Error {
public statusCode: number;
public statusMessage?: string;

View File

@ -276,6 +276,12 @@ export class Meta {
})
public remoteDriveCapacityMb: number;
@Column("integer", {
default: 5,
comment: "Antenna Limit",
})
public antennaLimit: number;
@Column("varchar", {
length: 128,
nullable: true,

View File

@ -24,6 +24,11 @@ export const meta = {
optional: false,
nullable: false,
},
antennaLimit: {
type: "number",
optional: false,
nullable: false,
},
cacheRemoteFiles: {
type: "boolean",
optional: false,
@ -487,6 +492,7 @@ export default define(meta, paramDef, async () => {
enableGuestTimeline: instance.enableGuestTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
antennaLimit: instance.antennaLimit,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,

View File

@ -94,6 +94,7 @@ export const paramDef = {
defaultDarkTheme: { type: "string", nullable: true },
localDriveCapacityMb: { type: "integer" },
remoteDriveCapacityMb: { type: "integer" },
antennaLimit: { type: "integer" },
cacheRemoteFiles: { type: "boolean" },
markLocalFilesNsfwByDefault: { type: "boolean" },
emailRequiredForSignup: { type: "boolean" },
@ -327,6 +328,10 @@ export default define(meta, paramDef, async (ps, me) => {
set.remoteDriveCapacityMb = ps.remoteDriveCapacityMb;
}
if (ps.antennaLimit !== undefined) {
set.antennaLimit = ps.antennaLimit;
}
if (ps.cacheRemoteFiles !== undefined) {
set.cacheRemoteFiles = ps.cacheRemoteFiles;
}

View File

@ -1,5 +1,5 @@
import define from "@/server/api/define.js";
import { genId } from "backend-rs";
import { fetchMeta, genId } from "backend-rs";
import { Antennas, UserLists, UserGroupJoinings } from "@/models/index.js";
import { ApiError } from "@/server/api/error.js";
import { publishInternalEvent } from "@/services/stream.js";
@ -109,10 +109,12 @@ export default define(meta, paramDef, async (ps, user) => {
let userList;
let userGroupJoining;
const instance = await fetchMeta(true);
const antennas = await Antennas.findBy({
userId: user.id,
});
if (antennas.length > 5 && !user.isAdmin) {
if (antennas.length >= instance.antennaLimit) {
throw new ApiError(meta.errors.tooManyAntennas);
}

View File

@ -126,6 +126,11 @@ export const meta = {
optional: false,
nullable: false,
},
antennaLimit: {
type: "number",
optional: false,
nullable: false,
},
cacheRemoteFiles: {
type: "boolean",
optional: false,
@ -445,6 +450,7 @@ export default define(meta, paramDef, async (ps, me) => {
enableGuestTimeline: instance.enableGuestTimeline,
driveCapacityPerLocalUserMb: instance.localDriveCapacityMb,
driveCapacityPerRemoteUserMb: instance.remoteDriveCapacityMb,
antennaLimit: instance.antennaLimit,
emailRequiredForSignup: instance.emailRequiredForSignup,
enableHcaptcha: instance.enableHcaptcha,
hcaptchaSiteKey: instance.hcaptchaSiteKey,

View File

@ -2,7 +2,16 @@ import { Feed } from "feed";
import { In, IsNull } from "typeorm";
import { config } from "@/config.js";
import type { User } from "@/models/entities/user.js";
import type { Note } from "@/models/entities/note.js";
import { Notes, DriveFiles, UserProfiles, Users } from "@/models/index.js";
import getNoteHtml from "@/remote/activitypub/misc/get-note-html.js";
/**
* If there is this part in the note, it will cause CDATA to be terminated early.
*/
function escapeCDATA(str: string) {
return str.replaceAll("]]>", "]]]]><![CDATA[>");
}
export default async function (
user: User,
@ -15,7 +24,7 @@ export default async function (
const author = {
link: `${config.url}/@${user.username}`,
email: `${user.username}@${config.host}`,
name: user.name || user.username,
name: escapeCDATA(user.name || user.username),
};
const profile = await UserProfiles.findOneByOrFail({ userId: user.id });
@ -44,11 +53,13 @@ export default async function (
title: `${author.name} (@${user.username}@${config.host})`,
updated: notes[0].createdAt,
generator: "Firefish",
description: `${user.notesCount} Notes, ${
profile.ffVisibility === "public" ? user.followingCount : "?"
} Following, ${
profile.ffVisibility === "public" ? user.followersCount : "?"
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
description: escapeCDATA(
`${user.notesCount} Notes, ${
profile.ffVisibility === "public" ? user.followingCount : "?"
} Following, ${
profile.ffVisibility === "public" ? user.followersCount : "?"
} Followers${profile.description ? ` · ${profile.description}` : ""}`,
),
link: author.link,
image: await Users.getAvatarUrl(user),
feedLinks: {
@ -88,19 +99,23 @@ export default async function (
}
feed.addItem({
title: title
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
.substring(0, 100),
title: escapeCDATA(
title
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
.substring(0, 100),
),
link: `${config.url}/notes/${note.id}`,
date: note.createdAt,
description: note.cw
? note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, "")
? escapeCDATA(note.cw.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""))
: undefined,
content: contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
content: escapeCDATA(
contentStr.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, ""),
),
});
}
async function noteToString(note, isTheNote = false) {
async function noteToString(note: Note, isTheNote = false) {
const author = isTheNote
? null
: await Users.findOneBy({ id: note.userId });
@ -135,7 +150,10 @@ export default async function (
}">${file.name}</a>`;
}
}
outstr += `${note.cw ? note.cw + "<br>" : ""}${note.text || ""}${fileEle}`;
outstr += `${note.cw ? note.cw + "<br>" : ""}${
getNoteHtml(note) || ""
}${fileEle}`;
if (isTheNote) {
outstr += ` <span class="${
note.renoteId ? "renote_note" : note.replyId ? "reply_note" : "new_note"

View File

@ -318,6 +318,8 @@ import XNoteSimple from "@/components/MkNoteSimple.vue";
import XNotePreview from "@/components/MkNotePreview.vue";
import XPostFormAttaches from "@/components/MkPostFormAttaches.vue";
import XPollEditor from "@/components/MkPollEditor.vue";
import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
import XMediaCaption from "@/components/MkMediaCaption.vue";
import { host, url } from "@/config";
import { erase, unique } from "@/scripts/array";
import { extractMentions } from "@/scripts/extract-mentions";
@ -334,7 +336,6 @@ import { getAccounts, openAccountMenu as openAccountMenu_ } from "@/account";
import { me } from "@/me";
import { uploadFile } from "@/scripts/upload";
import { deepClone } from "@/scripts/clone";
import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
import preprocess from "@/scripts/preprocess";
import { vibrate } from "@/scripts/vibrate";
import { langmap } from "@/scripts/langmap";
@ -508,6 +509,8 @@ const hashtags = computed(
defaultStore.makeGetterSetter("postFormHashtags"),
) as Ref<string | null>;
let isFirstPostAttempt = true;
watch(text, () => {
checkMissingMention();
});
@ -1022,6 +1025,46 @@ function deleteDraft() {
localStorage.setItem("drafts", JSON.stringify(draftData));
}
/**
* @returns whether the file is described
*/
function openFileDescriptionWindow(file: entities.DriveFile) {
return new Promise<boolean>((resolve, reject) => {
os.popup(
XMediaCaption,
{
title: i18n.ts.describeFile,
input: {
placeholder: i18n.ts.inputNewDescription,
default: file.comment !== null ? file.comment : "",
},
image: file,
},
{
done: (result) => {
if (!result || result.canceled) {
resolve(false);
return;
}
const comment = result.result?.length === 0 ? null : result.result;
os.api("drive/files/update", {
fileId: file.id,
comment,
})
.then(() => {
resolve(true);
file.comment = comment ?? null;
})
.catch((err: unknown) => {
reject(err);
});
},
},
"closed",
);
});
}
async function post() {
// For text that is too short, the false positive rate may be too high, so we don't show alarm.
if (defaultStore.state.autocorrectNoteLanguage && text.value.length > 10) {
@ -1056,6 +1099,24 @@ async function post() {
}
}
if (
defaultStore.state.showAddFileDescriptionAtFirstPost &&
files.value.some((f) => f.comment == null || f.comment.length === 0)
) {
if (isFirstPostAttempt) {
for (const file of files.value) {
if (file.comment == null || file.comment.length === 0) {
const described = await openFileDescriptionWindow(file);
if (!described) {
return;
}
}
}
isFirstPostAttempt = false;
return;
}
}
if (
defaultStore.state.showNoAltTextWarning &&
files.value.some((f) => f.comment == null || f.comment.length === 0)
@ -1064,12 +1125,22 @@ async function post() {
const { canceled } = await os.confirm({
type: "warning",
text: i18n.ts.noAltTextWarning,
okText: i18n.ts.goBack,
okText: i18n.ts.describeFile,
cancelText: i18n.ts.toPost,
isPlaintext: true,
});
if (!canceled) return;
if (!canceled) {
for (const file of files.value) {
if (file.comment == null || file.comment.length === 0) {
const described = await openFileDescriptionWindow(file);
if (!described) {
return;
}
}
}
return;
}
}
const processedText = preprocess(text.value);

View File

@ -350,6 +350,19 @@
</FormSplit>
</FormSection>
<FormSection>
<template #label>{{ i18n.ts.antennas }}</template>
<FormInput
v-model="antennaLimit"
type="number"
class="_formBlock"
>
<template #label>{{
i18n.ts.antennaLimit
}}</template>
</FormInput>
</FormSection>
<FormSection>
<template #label>ServiceWorker</template>
@ -502,6 +515,7 @@ const cacheRemoteFiles = ref(false);
const markLocalFilesNsfwByDefault = ref(false);
const localDriveCapacityMb = ref(0);
const remoteDriveCapacityMb = ref(0);
const antennaLimit = ref(0);
const enableRegistration = ref(false);
const emailRequiredForSignup = ref(false);
const enableServiceWorker = ref(false);
@ -579,6 +593,7 @@ async function init() {
markLocalFilesNsfwByDefault.value = meta.markLocalFilesNsfwByDefault;
localDriveCapacityMb.value = meta.driveCapacityPerLocalUserMb;
remoteDriveCapacityMb.value = meta.driveCapacityPerRemoteUserMb;
antennaLimit.value = meta.antennaLimit;
enableRegistration.value = !meta.disableRegistration;
emailRequiredForSignup.value = meta.emailRequiredForSignup;
enableServiceWorker.value = meta.enableServiceWorker;
@ -631,6 +646,7 @@ function save() {
markLocalFilesNsfwByDefault: markLocalFilesNsfwByDefault.value,
localDriveCapacityMb: localDriveCapacityMb.value,
remoteDriveCapacityMb: remoteDriveCapacityMb.value,
antennaLimit: antennaLimit.value,
disableRegistration: !enableRegistration.value,
emailRequiredForSignup: emailRequiredForSignup.value,
enableServiceWorker: enableServiceWorker.value,

View File

@ -4,30 +4,35 @@
><MkPageHeader :display-back-button="true"
/></template>
<MkSpacer :content-max="800">
<MkLoading v-if="!loaded" />
<MkPagination
v-else
ref="pagingComponent"
v-slot="{ items }"
:pagination="pagination"
>
<div ref="tlEl" class="giivymft noGap">
<XList
v-slot="{ item }"
:items="convertNoteEditsToNotes(items)"
class="notes"
:no-gap="true"
>
<XNote
:key="item.id"
class="qtqtichx"
:note="item"
:hide-footer="true"
:detailed-view="true"
/>
</XList>
</div>
</MkPagination>
<MkLoading v-if="note == null" />
<div v-else>
<MkRemoteCaution
v-if="note.user.host != null"
:href="note.url ?? note.uri!"
/>
<MkPagination
ref="pagingComponent"
v-slot="{ items }"
:pagination="pagination"
>
<div ref="tlEl" class="giivymft noGap">
<XList
v-slot="{ item }"
:items="convertNoteEditsToNotes(items)"
class="notes"
:no-gap="true"
>
<XNote
:key="item.id"
class="qtqtichx"
:note="item"
:hide-footer="true"
:detailed-view="true"
/>
</XList>
</div>
</MkPagination>
</div>
</MkSpacer>
</MkStickyContainer>
</template>
@ -44,6 +49,7 @@ import XNote from "@/components/MkNote.vue";
import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata";
import icon from "@/scripts/icon";
import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
const pagingComponent = ref<MkPaginationType<
typeof pagination.endpoint
@ -69,8 +75,7 @@ definePageMetadata(
})),
);
const note = ref<entities.Note>({} as entities.Note);
const loaded = ref(false);
const note = ref<entities.Note | null>(null);
onMounted(() => {
api("notes/show", {
@ -83,20 +88,19 @@ onMounted(() => {
res.replyId = null;
note.value = res;
loaded.value = true;
});
});
function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
const now: entities.NoteEdit = {
id: "EditionNow",
noteId: note.value.id,
updatedAt: note.value.createdAt,
text: note.value.text,
cw: note.value.cw,
files: note.value.files,
fileIds: note.value.fileIds,
emojis: note.value.emojis,
noteId: note.value!.id,
updatedAt: note.value!.createdAt,
text: note.value!.text,
cw: note.value!.cw,
files: note.value!.files,
fileIds: note.value!.fileIds,
emojis: note.value!.emojis,
};
return [now]
@ -112,7 +116,7 @@ function convertNoteEditsToNotes(noteEdits: entities.NoteEdit[]) {
_shouldInsertAd_: false,
files: noteEdit.files,
fileIds: noteEdit.fileIds,
emojis: note.value.emojis.concat(noteEdit.emojis),
emojis: note.value!.emojis.concat(noteEdit.emojis),
});
});
}

View File

@ -124,6 +124,9 @@
<FormSwitch v-model="showNoAltTextWarning" class="_formBlock">{{
i18n.ts.showNoAltTextWarning
}}</FormSwitch>
<FormSwitch v-model="showAddFileDescriptionAtFirstPost" class="_formBlock">{{
i18n.ts.showAddFileDescriptionAtFirstPost
}}</FormSwitch>
<FormSwitch v-model="autocorrectNoteLanguage" class="_formBlock">{{
i18n.ts.autocorrectNoteLanguage
}}</FormSwitch>
@ -533,6 +536,9 @@ const pullToRefreshThreshold = computed(
const showNoAltTextWarning = computed(
defaultStore.makeGetterSetter("showNoAltTextWarning"),
);
const showAddFileDescriptionAtFirstPost = computed(
defaultStore.makeGetterSetter("showAddFileDescriptionAtFirstPost"),
);
const autocorrectNoteLanguage = computed(
defaultStore.makeGetterSetter("autocorrectNoteLanguage"),
);

View File

@ -125,6 +125,7 @@ const defaultStoreSaveKeys: (keyof (typeof defaultStore)["state"])[] = [
"enablePullToRefresh",
"pullToRefreshThreshold",
"showNoAltTextWarning",
"showAddFileDescriptionAtFirstPost",
"autocorrectNoteLanguage",
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [

View File

@ -442,6 +442,10 @@ export const defaultStore = markRaw(
where: "account",
default: true,
},
showAddFileDescriptionAtFirstPost: {
where: "account",
default: false,
},
autocorrectNoteLanguage: {
where: "account",
default: true,

View File

@ -356,6 +356,7 @@ export type LiteInstanceMetadata = {
disableGlobalTimeline: boolean;
driveCapacityPerLocalUserMb: number;
driveCapacityPerRemoteUserMb: number;
antennaLimit: number;
enableHcaptcha: boolean;
hcaptchaSiteKey: string | null;
enableRecaptcha: boolean;