Merge branch 'develop' into refactor/push-notification

This commit is contained in:
naskya 2024-04-27 06:06:53 +09:00
commit ff96c20893
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
18 changed files with 699 additions and 656 deletions

18
Cargo.lock generated
View file

@ -568,9 +568,9 @@ dependencies = [
[[package]]
name = "concurrent-queue"
version = "2.4.0"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16048cd947b08fa32c24458a22f5dc5e835264f689f4f5653210c69fd107363"
checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
dependencies = [
"crossbeam-utils",
]
@ -1042,9 +1042,9 @@ checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
name = "flate2"
version = "1.0.28"
version = "1.0.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
checksum = "4556222738635b7a3417ae6130d8f52201e45a0c4d1a907f0826383adb5f85e7"
dependencies = [
"crc32fast",
"miniz_oxide",
@ -3030,9 +3030,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.11"
version = "0.21.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e"
dependencies = [
"ring",
"rustls-webpki",
@ -4080,7 +4080,7 @@ dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.6.6",
"winnow 0.6.7",
]
[[package]]
@ -4640,9 +4640,9 @@ dependencies = [
[[package]]
name = "winnow"
version = "0.6.6"
version = "0.6.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0c976aaaa0e1f90dbb21e9587cdaf1d9679a1cde8875c0d6bd83ab96a208352"
checksum = "14b9415ee827af173ebb3f15f9083df5a122eb93572ec28741fb153356ea2578"
dependencies = [
"memchr",
]

View file

@ -5,24 +5,24 @@ resolver = "2"
[workspace.dependencies]
macro_rs = { path = "packages/macro-rs" }
napi = { version = "2.16.2", default-features = false }
napi-derive = "2.16.2"
napi = { version = "2.16.4", default-features = false }
napi-derive = "2.16.3"
napi-build = "2.1.3"
argon2 = "0.5.3"
basen = "0.1.0"
bcrypt = "0.15.1"
chrono = "0.4.37"
chrono = "0.4.38"
convert_case = "0.6.0"
cuid2 = "0.1.2"
emojis = "0.6.1"
emojis = "0.6.2"
idna = "0.5.0"
image = "0.25.1"
nom-exif = "1.2.0"
once_cell = "1.19.0"
openssl = "0.10.64"
pretty_assertions = "1.4.0"
proc-macro2 = "1.0.79"
proc-macro2 = "1.0.81"
quote = "1.0.36"
rand = "0.8.5"
redis = "0.25.3"
@ -30,15 +30,15 @@ regex = "1.10.4"
reqwest = "0.12.4"
rmp-serde = "1.2.0"
sea-orm = "0.12.15"
serde = "1.0.197"
serde_json = "1.0.115"
serde = "1.0.198"
serde_json = "1.0.116"
serde_yaml = "0.9.34"
strum = "0.26.2"
syn = "2.0.58"
thiserror = "1.0.58"
syn = "2.0.60"
thiserror = "1.0.59"
tokio = "1.37.0"
tracing = "0.1.40"
tracing-subscriber = "0.3.1"
tracing-subscriber = "0.3.18"
url = "2.5.0"
urlencoding = "2.1.3"
web-push = "0.10.1"

View file

@ -5,6 +5,12 @@ Critical security updates are indicated by the :warning: icon.
- Server administrators should check [notice-for-admins.md](./notice-for-admins.md) as well.
- Third-party client/bot developers may want to check [api-change.md](./api-change.md) as well.
## Unreleased
- Add features to share links to an account in the three dots menu on the profile page
- Improve server logs
- Fix bugs
## [v20240424](https://firefish.dev/firefish/firefish/-/merge_requests/10765/commits)
- Improve the usability of the feature to prevent forgetting to write alt texts

View file

@ -1825,6 +1825,7 @@ _notification:
reacted: mereaksi postinganmu
renoted: memposting ulang postinganmu
voted: memilih di angketmu
andCountUsers: dan {count} lebih banyak pengguna {acted}
_deck:
alwaysShowMainColumn: "Selalu tampilkan kolom utama"
columnAlign: "Luruskan kolom"
@ -2267,3 +2268,13 @@ markLocalFilesNsfwByDefaultDescription: Terlepas dari pengaturan ini, pengguna d
menghapus sendiri tanda NSFW. Berkas yang ada tidak berpengaruh.
noteEditHistory: Riwayat penyuntingan kiriman
media: Media
antennaLimit: Jumlah antena maksimum yang dapat dibuat oleh setiap pengguna
showAddFileDescriptionAtFirstPost: Buka formulir secara otomatis untuk menulis deskripsi
ketika mencoba mengirim berkas tanpa deskripsi
remoteFollow: Ikuti jarak jauh
foldNotification: Kelompokkan notifikasi yang sama
getQrCode: Tampilkan kode QR
cannotEditVisibility: Kamu tidak bisa menyunting keterlihatan
useThisAccountConfirm: Apakah kamu ingin melanjutkan dengan akun ini?
inputAccountId: Silakan memasukkan akunmu (misalnya, @firefish@info.firefish.dev)
copyRemoteFollowUrl: Salin URL ikuti jarak jauh

View file

@ -1902,6 +1902,7 @@ _notification:
reacted: がリアクションしました
renoted: がブーストしました
voted: が投票しました
andCountUsers: と{count}人が{acted}しました
_deck:
alwaysShowMainColumn: "常にメインカラムを表示"
columnAlign: "カラムの寄せ"
@ -2059,3 +2060,10 @@ markLocalFilesNsfwByDefaultDescription: この設定が有効でも、ユーザ
noteEditHistory: 編集履歴
showAddFileDescriptionAtFirstPost: 説明の無い添付ファイルを投稿しようとした際に説明を書く画面を自動で開く
antennaLimit: 各ユーザーが作れるアンテナの最大数
inputAccountId: 'あなたのアカウントを入力してください(例: @firefish@info.firefish.dev'
remoteFollow: リモートフォロー
cannotEditVisibility: 公開範囲は変更できません
useThisAccountConfirm: このアカウントで操作を続けますか?
getQrCode: QRコードを表示
copyRemoteFollowUrl: リモートからフォローするURLをコピー
foldNotification: 同じ種類の通知をまとめて表示する

View file

@ -45,11 +45,11 @@
"js-yaml": "4.1.0"
},
"devDependencies": {
"@biomejs/biome": "1.6.4",
"@biomejs/cli-darwin-arm64": "^1.6.4",
"@biomejs/cli-darwin-x64": "^1.6.4",
"@biomejs/cli-linux-arm64": "^1.6.4",
"@biomejs/cli-linux-x64": "^1.6.4",
"@biomejs/biome": "1.7.1",
"@biomejs/cli-darwin-arm64": "^1.7.1",
"@biomejs/cli-darwin-x64": "^1.7.1",
"@biomejs/cli-linux-arm64": "^1.7.1",
"@biomejs/cli-linux-x64": "^1.7.1",
"@types/node": "20.12.7",
"execa": "8.0.1",
"pnpm": "8.15.7",

View file

@ -22,21 +22,21 @@
"@swc/core-android-arm64": "1.3.11"
},
"dependencies": {
"@bull-board/api": "5.15.5",
"@bull-board/koa": "5.15.5",
"@bull-board/ui": "5.15.5",
"@bull-board/api": "5.16.0",
"@bull-board/koa": "5.16.0",
"@bull-board/ui": "5.16.0",
"@discordapp/twemoji": "^15.0.3",
"@koa/cors": "5.0.0",
"@koa/multer": "3.0.2",
"@koa/router": "12.0.1",
"@ladjs/koa-views": "9.0.0",
"@peertube/http-signature": "1.7.0",
"@redocly/openapi-core": "1.11.0",
"@redocly/openapi-core": "1.12.0",
"@sinonjs/fake-timers": "11.2.2",
"adm-zip": "0.5.10",
"ajv": "8.12.0",
"archiver": "7.0.1",
"aws-sdk": "2.1599.0",
"aws-sdk": "2.1608.0",
"axios": "^1.6.8",
"backend-rs": "workspace:*",
"blurhash": "2.0.5",
@ -61,7 +61,7 @@
"gunzip-maybe": "^1.4.2",
"happy-dom": "^14.7.1",
"hpagent": "1.2.0",
"ioredis": "5.3.2",
"ioredis": "5.4.1",
"ip-cidr": "4.0.0",
"is-svg": "5.0.0",
"json5": "2.2.3",
@ -124,7 +124,7 @@
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.4.13",
"@swc/core": "1.5.0",
"@types/adm-zip": "^0.5.5",
"@types/color-convert": "^2.0.3",
"@types/content-disposition": "^0.5.8",
@ -154,7 +154,7 @@
"@types/pug": "2.0.10",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.5",
"@types/qs": "6.9.14",
"@types/qs": "6.9.15",
"@types/random-seed": "0.3.5",
"@types/ratelimiter": "3.4.6",
"@types/rename": "1.0.7",
@ -169,7 +169,7 @@
"@types/websocket": "1.0.10",
"@types/ws": "8.5.10",
"cross-env": "7.0.3",
"eslint": "^9.0.0",
"eslint": "^9.1.1",
"mocha": "10.4.0",
"pug": "3.0.2",
"strict-event-emitter-types": "2.0.0",
@ -177,7 +177,7 @@
"ts-loader": "9.5.1",
"ts-node": "10.9.2",
"tsconfig-paths": "4.2.0",
"type-fest": "4.15.0",
"type-fest": "4.17.0",
"typescript": "5.4.5",
"webpack": "^5.91.0",
"ws": "8.16.0"

View file

@ -47,8 +47,8 @@ export default class Logger {
return logger;
}
private showThisLog(logLevel: Level, configLevel: string) {
switch (configLevel) {
private showThisLog(logLevel: Level, configMaxLevel: string) {
switch (configMaxLevel) {
case "error":
return ["error"].includes(logLevel);
case "warning":
@ -75,7 +75,10 @@ export default class Logger {
if (
(config.maxLogLevel != null &&
!this.showThisLog(level, config.maxLogLevel)) ||
(config.logLevel != null && !config.logLevel.includes(level))
(config.logLevel != null && !config.logLevel.includes(level)) ||
(config.maxLogLevel == null &&
config.logLevel == null &&
!this.showThisLog(level, "info"))
)
return;
if (!this.store) store = false;

View file

@ -28,14 +28,14 @@
"@types/matter-js": "0.19.6",
"@types/prismjs": "^1.26.3",
"@types/punycode": "2.1.4",
"@types/qrcode": "1.5.1",
"@types/qrcode": "1.5.5",
"@types/seedrandom": "3.0.8",
"@types/textarea-caret": "^3.0.3",
"@types/throttle-debounce": "5.0.2",
"@types/tinycolor2": "1.4.6",
"@types/uuid": "9.0.8",
"@vitejs/plugin-vue": "5.0.4",
"@vue/runtime-core": "3.4.21",
"@vue/runtime-core": "3.4.25",
"autobind-decorator": "2.4.0",
"autosize": "6.0.1",
"broadcast-channel": "7.0.0",
@ -65,14 +65,14 @@
"libopenmpt-wasm": "github:TheEssem/libopenmpt-packaging#build",
"matter-js": "0.19.0",
"mfm-js": "0.24.0",
"multer": "1.4.4-lts.1",
"multer": "1.4.5-lts.1",
"moment": "2.30.1",
"photoswipe": "5.4.3",
"prismjs": "1.29.0",
"punycode": "2.3.1",
"qrcode": "1.5.3",
"qrcode-vue3": "^1.6.8",
"rollup": "4.14.2",
"rollup": "4.16.4",
"s-age": "1.1.2",
"sass": "1.75.0",
"seedrandom": "3.0.5",
@ -80,19 +80,19 @@
"swiper": "11.1.1",
"syuilo-password-strength": "0.0.1",
"textarea-caret": "3.1.0",
"three": "0.163.0",
"three": "0.164.1",
"throttle-debounce": "5.0.0",
"tinycolor2": "1.6.0",
"tinyld": "^1.3.4",
"typescript": "5.4.5",
"unicode-emoji-json": "^0.6.0",
"uuid": "9.0.1",
"vite": "5.2.8",
"vite": "5.2.10",
"vite-plugin-compression": "^0.5.1",
"vue": "3.4.21",
"vue": "3.4.25",
"vue-draggable-plus": "^0.4.0",
"vue-plyr": "^7.0.0",
"vue-prism-editor": "2.0.0-alpha.2",
"vue-tsc": "2.0.13"
"vue-tsc": "2.0.14"
}
}

View file

@ -26,7 +26,6 @@
: notification.reaction
"
:custom-emojis="notification.note.emojis"
:no-style="true"
/>
<XReactionIcon
v-else-if="
@ -73,7 +72,7 @@
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref } from "vue";
import type { Connection } from "firefish-js/src/streaming";
import type { Channels } from "firefish-js/src/streaming.types";
import XReactionIcon from "@/components/MkReactionIcon.vue";
@ -116,8 +115,10 @@ const defaultReaction = ["⭐", "👍", "❤️"].includes(instance.defaultReact
? instance.defaultReaction
: "⭐";
const users = ref(props.notification.users.slice(0, 5));
const userleft = ref(props.notification.users.length - users.value.length);
const users = computed(() => props.notification.users.slice(0, 5));
const userleft = computed(
() => props.notification.users.length - users.value.length,
);
let readObserver: IntersectionObserver | undefined;
let connection: Connection<Channels["main"]> | null = null;

View file

@ -1,5 +1,9 @@
<template>
<MkPagination ref="pagingComponent" :pagination="pagination">
<MkPagination
ref="pagingComponent"
:pagination="pagination"
:folder="convertNotification"
>
<template #empty>
<div class="_fullinfo">
<img
@ -11,9 +15,9 @@
</div>
</template>
<template #default="{ items: notifications }">
<template #default="{ foldedItems: notifications }">
<XList
:items="convertNotification(notifications)"
:items="notifications"
v-slot="{ item: notification }"
class="elsfgstc"
:no-gap="true"
@ -92,7 +96,7 @@ const pagination = Object.assign(
},
shouldFold
? {
limit: FETCH_LIMIT,
limit: 50,
secondFetchLimit: FETCH_LIMIT,
}
: {
@ -134,11 +138,11 @@ const onNotification = (notification: entities.Notification) => {
let connection: StreamTypes.ChannelOf<"main"> | undefined;
function convertNotification(n: entities.Notification[]) {
function convertNotification(ns: entities.Notification[]) {
if (shouldFold) {
return foldNotifications(n, FETCH_LIMIT);
return foldNotifications(ns);
} else {
return n;
return ns;
}
}

View file

@ -38,7 +38,7 @@
</MkButton>
<MkLoading v-else class="loading" />
</div>
<slot :items="items"></slot>
<slot :items="items" :foldedItems="foldedItems"></slot>
<div
v-show="!pagination.reversed && more"
key="_more_"
@ -66,8 +66,8 @@
</transition>
</template>
<script lang="ts" setup generic="E extends PagingKey">
import type { ComponentPublicInstance, ComputedRef } from "vue";
<script lang="ts" setup generic="E extends PagingKey, Fold extends PagingAble">
import type { ComponentPublicInstance, ComputedRef, Ref } from "vue";
import {
computed,
isRef,
@ -79,12 +79,7 @@ import {
} from "vue";
import type { Endpoints, TypeUtils } from "firefish-js";
import * as os from "@/os";
import {
getScrollContainer,
getScrollPosition,
isTopVisible,
onScrollTop,
} from "@/scripts/scroll";
import { isTopVisible, onScrollTop } from "@/scripts/scroll";
import MkButton from "@/components/MkButton.vue";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";
@ -105,11 +100,15 @@ export type MkPaginationType<
reload: () => Promise<void>;
refresh: () => Promise<void>;
prepend: (item: Item) => Promise<void>;
append: (item: Item) => Promise<void>;
append: (...item: Item[]) => Promise<void>;
removeItem: (finder: (item: Item) => boolean) => boolean;
updateItem: (id: string, replacer: (old: Item) => Item) => boolean;
};
export type PagingAble = {
id: string;
};
export type PagingKeyOf<T> = TypeUtils.EndpointsOf<T[]>;
// biome-ignore lint/suspicious/noExplicitAny: Used Intentionally
export type PagingKey = PagingKeyOf<any>;
@ -142,13 +141,18 @@ export interface Paging<E extends PagingKey = PagingKey> {
export type PagingOf<T> = Paging<TypeUtils.EndpointsOf<T[]>>;
type Item = Endpoints[E]["res"][number];
type Param = Endpoints[E]["req"] | Record<string, never>;
const SECOND_FETCH_LIMIT_DEFAULT = 30;
const FIRST_FETCH_LIMIT_DEFAULT = 10;
const props = withDefaults(
defineProps<{
pagination: Paging<E>;
disableAutoLoad?: boolean;
displayLimit?: number;
folder?: (i: Item[]) => Fold[];
}>(),
{
displayLimit: 30,
@ -156,7 +160,7 @@ const props = withDefaults(
);
const slots = defineSlots<{
default(props: { items: Item[] }): unknown;
default(props: { items: Item[]; foldedItems: Fold[] }): unknown;
empty(props: Record<string, never>): never;
}>();
@ -165,13 +169,59 @@ const emit = defineEmits<{
(ev: "status", hasError: boolean): void;
}>();
type Param = Endpoints[E]["req"] | Record<string, never>;
type Item = Endpoints[E]["res"][number];
const rootEl = ref<HTMLElement>();
const items = ref<Item[]>([]);
const foldedItems = ref([]) as Ref<Fold[]>;
// To improve performance, we do not use vues `computed` here
function calculateItems() {
function getItems<T>(folder: (ns: Item[]) => T[]) {
const res = [
folder(prepended.value.toReversed()),
...arrItems.value.map((arr) => folder(arr)),
folder(appended.value),
].flat(1);
if (props.pagination.reversed) {
res.reverse();
}
return res;
}
items.value = getItems((x) => x);
if (props.folder) foldedItems.value = getItems(props.folder);
}
const queue = ref<Item[]>([]);
/**
* The cached elements inserted front by `prepend` function
*/
const prepended = ref<Item[]>([]);
/**
* The array of "frozen" items
*/
const arrItems = ref<Item[][]>([]);
/**
* The cached elements inserted back by `append` function
*/
const appended = ref<Item[]>([]);
const idMap = new Map<string, boolean>();
const offset = ref(0);
type PagingByParam =
| {
offset: number;
}
| {
sinceId: string;
}
| {
untilId: string;
}
| Record<string, never>;
let nextPagingBy: PagingByParam = {};
const fetching = ref(true);
const moreFetching = ref(false);
const more = ref(false);
@ -184,54 +234,14 @@ const init = async (): Promise<void> => {
queue.value = [];
fetching.value = true;
const params = props.pagination.params ? unref(props.pagination.params) : {};
await os
.api(props.pagination.endpoint, {
...params,
limit: props.pagination.noPaging
? props.pagination.limit || 10
: (props.pagination.limit || 10) + 1,
...(props.pagination.ascending
? {
// An initial value smaller than all possible ids must be filled in here.
sinceId: "0",
}
: {}),
})
.then(
(res: Item[]) => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 2) item._shouldInsertAd_ = true;
} else {
if (i === 3) item._shouldInsertAd_ = true;
}
}
if (
!props.pagination.noPaging &&
res.length > (props.pagination.limit || 10)
) {
res.pop();
items.value = props.pagination.reversed ? res.toReversed() : res;
more.value = true;
} else {
items.value = props.pagination.reversed ? res.toReversed() : res;
more.value = false;
}
offset.value = res.length;
error.value = false;
fetching.value = false;
},
(_err) => {
error.value = true;
fetching.value = false;
},
);
await fetch(true);
};
const reload = (): Promise<void> => {
items.value = [];
arrItems.value = [];
appended.value = [];
prepended.value = [];
idMap.clear();
return init();
};
@ -240,30 +250,18 @@ const refresh = async (): Promise<void> => {
await os
.api(props.pagination.endpoint, {
...params,
limit: items.value.length + 1,
limit: (items.value.length || foldedItems.value.length) + 1,
offset: 0,
})
.then(
(res: Item[]) => {
const ids = items.value.reduce(
(a, b) => {
a[b.id] = true;
return a;
},
{} as Record<string, boolean>,
);
appended.value = [];
prepended.value = [];
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (!updateItem(item.id, (_old) => item)) {
append(item);
}
delete ids[item.id];
}
// appended should be inserted into arrItems to fix the element position
arrItems.value = [res];
for (const id in ids) {
removeItem((i) => i.id === id);
}
calculateItems();
},
(_err) => {
error.value = true;
@ -272,155 +270,145 @@ const refresh = async (): Promise<void> => {
);
};
const fetchMore = async (): Promise<void> => {
if (
!more.value ||
fetching.value ||
moreFetching.value ||
items.value.length === 0
)
return;
moreFetching.value = true;
backed.value = true;
async function fetch(firstFetching?: boolean) {
let limit: number;
if (firstFetching) {
limit = props.pagination.noPaging
? props.pagination.limit || FIRST_FETCH_LIMIT_DEFAULT
: (props.pagination.limit || FIRST_FETCH_LIMIT_DEFAULT) + 1;
if (props.pagination.ascending) {
nextPagingBy = {
// An initial value smaller than all possible ids must be filled in here.
sinceId: "0",
};
}
} else {
if (
!more.value ||
fetching.value ||
moreFetching.value ||
items.value.length === 0
)
return;
moreFetching.value = true;
backed.value = true;
limit =
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1;
}
const params = props.pagination.params ? unref(props.pagination.params) : {};
await os
.api(props.pagination.endpoint, {
...params,
limit:
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1,
...(props.pagination.offsetMode
? {
offset: offset.value,
}
: props.pagination.reversed
? {
sinceId: items.value[0].id,
}
: props.pagination.ascending
? {
sinceId: items.value[items.value.length - 1].id,
}
: {
untilId: items.value[items.value.length - 1].id,
}),
limit,
...nextPagingBy,
})
.then(
(res: Item[]) => {
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - 9) item._shouldInsertAd_ = true;
} else {
if (i === 10) item._shouldInsertAd_ = true;
if (!props.pagination.reversed)
for (let i = 0; i < res.length; i++) {
const item = res[i];
if (props.pagination.reversed) {
if (i === res.length - (firstFetching ? 2 : 9))
item._shouldInsertAd_ = true;
} else {
if (i === (firstFetching ? 3 : 10)) item._shouldInsertAd_ = true;
}
}
}
if (
res.length >
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT)
) {
if (!props.pagination.noPaging && res.length > limit - 1) {
res.pop();
items.value = props.pagination.reversed
? res.toReversed().concat(items.value)
: items.value.concat(res);
more.value = true;
} else {
items.value = props.pagination.reversed
? res.toReversed().concat(items.value)
: items.value.concat(res);
more.value = false;
}
offset.value += res.length;
error.value = false;
fetching.value = false;
moreFetching.value = false;
const lastRes = res[res.length - 1];
if (props.pagination.offsetMode) {
nextPagingBy = {
offset: offset.value,
};
} else if (props.pagination.ascending) {
nextPagingBy = {
sinceId: lastRes?.id,
};
} else {
nextPagingBy = {
untilId: lastRes?.id,
};
}
if (firstFetching && props.folder != null) {
// In this way, prepended has some initial values for folding
prepended.value = res.toReversed();
} else {
// For ascending and offset modes, append and prepend may cause item duplication
// so they need to be filtered out.
if (props.pagination.offsetMode || props.pagination.ascending) {
for (const item of appended.value) {
idMap.set(item.id, true);
}
// biome-ignore lint/style/noParameterAssign: assign it intentially
res = res.filter((item) => {
if (idMap.has(item)) return false;
idMap.set(item, true);
return true;
});
}
// appended should be inserted into arrItems to fix the element position
arrItems.value.push(appended.value);
arrItems.value.push(res);
appended.value = [];
}
calculateItems();
},
(_err) => {
error.value = true;
fetching.value = false;
moreFetching.value = false;
},
);
}
const fetchMore = async (): Promise<void> => {
await fetch();
};
const fetchMoreAhead = async (): Promise<void> => {
if (
!more.value ||
fetching.value ||
moreFetching.value ||
items.value.length === 0
)
return;
moreFetching.value = true;
const params = props.pagination.params ? unref(props.pagination.params) : {};
await os
.api(props.pagination.endpoint, {
...params,
limit:
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT) + 1,
...(props.pagination.offsetMode
? {
offset: offset.value,
}
: props.pagination.reversed
? {
untilId: items.value[0].id,
}
: {
sinceId: items.value[items.value.length - 1].id,
}),
})
.then(
(res: Item[]) => {
if (
res.length >
(props.pagination.secondFetchLimit ?? SECOND_FETCH_LIMIT_DEFAULT)
) {
res.pop();
items.value = props.pagination.reversed
? res.toReversed().concat(items.value)
: items.value.concat(res);
more.value = true;
} else {
items.value = props.pagination.reversed
? res.toReversed().concat(items.value)
: items.value.concat(res);
more.value = false;
}
offset.value += res.length;
moreFetching.value = false;
},
(_err) => {
moreFetching.value = false;
},
);
await fetch();
};
const prepend = (item: Item): void => {
const prepend = (...item: Item[]): void => {
// If there are too many prepended, merge them into arrItems
if (
prepended.value.length >
(props.pagination.secondFetchLimit || SECOND_FETCH_LIMIT_DEFAULT)
) {
arrItems.value.unshift(prepended.value.toReversed());
prepended.value = [];
// We don't need to calculate here because it won't cause any changes in items
}
if (props.pagination.reversed) {
if (rootEl.value) {
const container = getScrollContainer(rootEl.value);
if (container == null) {
// TODO?
} else {
const pos = getScrollPosition(rootEl.value);
const viewHeight = container.clientHeight;
const height = container.scrollHeight;
const isBottom = pos + viewHeight > height - 32;
if (isBottom) {
//
if (items.value.length >= props.displayLimit) {
// Vue 3.2
// items.value = items.value.slice(-props.displayLimit);
while (items.value.length >= props.displayLimit) {
items.value.shift();
}
more.value = true;
}
}
}
}
items.value.push(item);
// TODO
prepended.value.push(...item);
calculateItems();
} else {
// unshiftOK
// When displaying for the first time, just do this is OK
if (!rootEl.value) {
items.value.unshift(item);
prepended.value.push(...item);
calculateItems();
return;
}
@ -429,52 +417,63 @@ const prepend = (item: Item): void => {
(document.body.contains(rootEl.value) && isTopVisible(rootEl.value));
if (isTop) {
// Prepend the item
items.value.unshift(item);
//
if (items.value.length >= props.displayLimit) {
// Vue 3.2
// this.items = items.value.slice(0, props.displayLimit);
while (items.value.length >= props.displayLimit) {
items.value.pop();
}
more.value = true;
}
prepended.value.push(...item);
calculateItems();
} else {
queue.value.push(item);
queue.value.push(...item);
onScrollTop(rootEl.value, () => {
for (const queueItem of queue.value) {
prepend(queueItem);
}
prepend(...queue.value);
queue.value = [];
});
}
}
};
const append = (item: Item): void => {
items.value.push(item);
const append = (...items: Item[]): void => {
appended.value.push(...items);
calculateItems();
};
const _removeItem = (arr: Item[], finder: (item: Item) => boolean): boolean => {
const i = arr.findIndex(finder);
if (i === -1) {
return false;
}
arr.splice(i, 1);
return true;
};
const _updateItem = (
arr: Item[],
id: Item["id"],
replacer: (old: Item) => Item,
): boolean => {
const i = arr.findIndex((item) => item.id === id);
if (i === -1) {
return false;
}
arr[i] = replacer(arr[i]);
return true;
};
const removeItem = (finder: (item: Item) => boolean): boolean => {
const i = items.value.findIndex(finder);
if (i === -1) {
return false;
}
items.value.splice(i, 1);
return true;
const res =
_removeItem(prepended.value, finder) ||
_removeItem(appended.value, finder) ||
arrItems.value.filter((arr) => _removeItem(arr, finder)).length > 0;
calculateItems();
return res;
};
const updateItem = (id: Item["id"], replacer: (old: Item) => Item): boolean => {
const i = items.value.findIndex((item) => item.id === id);
if (i === -1) {
return false;
}
items.value[i] = replacer(items.value[i]);
return true;
const res =
_updateItem(prepended.value, id, replacer) ||
_updateItem(appended.value, id, replacer) ||
arrItems.value.filter((arr) => _updateItem(arr, id, replacer)).length > 0;
calculateItems();
return res;
};
if (props.pagination.params && isRef<Param>(props.pagination.params)) {

View file

@ -338,7 +338,7 @@ defineExpose({
content: "";
position: absolute;
inset: -2px 0;
border: 2px solid var(--accentDarken);
border-bottom: 2px solid var(--accentDarken);
mask: linear-gradient(
to right,
transparent,

View file

@ -20,7 +20,6 @@ interface FoldOption {
*/
export function foldItems<ItemFolded, Item>(
ns: Item[],
fetch_limit: number,
classfier: (n: Item, index: number) => string,
aggregator: (ns: Item[], key: string) => ItemFolded,
_options?: FoldOption,
@ -30,55 +29,48 @@ export function foldItems<ItemFolded, Item>(
const options: FoldOption = _options ?? {};
options.skipSingleElement ??= true;
for (let i = 0; i < ns.length; i += fetch_limit) {
const toFold = ns.slice(i, i + fetch_limit);
const toAppendKeys: string[] = [];
const foldMap = new Map<string, Item[]>();
const toAppendKeys: string[] = [];
const foldMap = new Map<string, Item[]>();
for (const [index, n] of toFold.entries()) {
const key = classfier(n, index);
const arr = foldMap.get(key);
if (arr != null) {
arr.push(n);
} else {
foldMap.set(key, [n]);
toAppendKeys.push(key);
}
for (const [index, n] of ns.entries()) {
const key = classfier(n, index);
const arr = foldMap.get(key);
if (arr != null) {
arr.push(n);
} else {
foldMap.set(key, [n]);
toAppendKeys.push(key);
}
res = res.concat(
toAppendKeys.map((key) => {
const arr = foldMap.get(key)!;
if (arr?.length === 1 && options?.skipSingleElement) {
return arr[0];
}
return aggregator(arr, key);
}),
);
}
res = toAppendKeys.map((key) => {
const arr = foldMap.get(key)!;
if (arr?.length === 1 && options?.skipSingleElement) {
return arr[0];
}
return aggregator(arr, key);
});
return res;
}
export function foldNotifications(
ns: entities.Notification[],
fetch_limit: number,
) {
export function foldNotifications(ns: entities.Notification[]) {
// By the implement of MkPagination, lastId is unique and is safe for key
const lastId = ns[ns.length - 1]?.id ?? "prepend";
return foldItems(
ns,
fetch_limit,
(n) => {
switch (n.type) {
case "renote":
return `renote-of:${n.note.renote.id}`;
return `renote-${n.note.renote.id}`;
case "reaction":
return `reaction:${n.reaction}:of:${n.note.id}`;
return `reaction-${n.reaction}-of-${n.note.id}`;
default: {
return `${n.id}`;
}
}
},
(ns) => {
(ns, key) => {
const represent = ns[0];
function check(
ns: entities.Notification[],
@ -94,6 +86,7 @@ export function foldNotifications(
userIds: ns.map((nn) => nn.userId),
users: ns.map((nn) => nn.user),
notifications: ns!,
id: `G-${lastId}-${key}`,
} as NotificationFolded;
},
);

View file

@ -4,6 +4,9 @@ export function getScrollContainer(el: HTMLElement | null): HTMLElement | null {
if (el == null) return null;
const overflow = window.getComputedStyle(el).getPropertyValue("overflow-y");
if (overflow === "scroll" || overflow === "auto") {
if (el.tagName === "HTML") {
return null;
}
return el;
} else {
return getScrollContainer(el.parentElement);

View file

@ -21,7 +21,7 @@
},
"devDependencies": {
"@swc/cli": "0.3.12",
"@swc/core": "1.4.13",
"@swc/core": "1.5.0",
"@swc/types": "^0.1.6",
"@types/jest": "^29.5.12",
"@types/node": "20.12.7",

View file

@ -11,7 +11,7 @@
"devDependencies": {
"firefish-js": "workspace:*",
"idb-keyval": "^6.2.1",
"vite": "5.2.8",
"vite": "5.2.10",
"vite-plugin-compression": "^0.5.1"
}
}

File diff suppressed because it is too large Load diff