Compare commits
9 Commits
48bfadde44
...
c6ff726de5
Author | SHA1 | Date |
---|---|---|
laozhoubuluo | c6ff726de5 | |
naskya | 3af8f86924 | |
naskya | 276cabbbe3 | |
naskya | af14bee31f | |
naskya | b3d1be457b | |
Hosted Weblate | 347851d6bb | |
jolupa | abec71074b | |
Lhcfl | 272e30be0c | |
老周部落 | 508cb25f54 |
|
@ -56,10 +56,6 @@ packages/backend/assets/instance.css
|
|||
packages/backend/assets/sounds/None.mp3
|
||||
packages/backend/assets/LICENSE
|
||||
|
||||
!/packages/backend/queue/processors/db
|
||||
!/packages/backend/src/db
|
||||
!/packages/backend/src/server/api/endpoints/drive/files
|
||||
|
||||
packages/megalodon/lib
|
||||
packages/megalodon/.idea
|
||||
|
||||
|
|
|
@ -185,6 +185,7 @@ cargo_clippy:
|
|||
when: never
|
||||
services: []
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get install -y --no-install-recommends build-essential clang mold perl
|
||||
- cp ci/cargo/config.toml /usr/local/cargo/config.toml
|
||||
- rustup component add clippy
|
||||
|
|
|
@ -5,6 +5,11 @@ 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
|
||||
|
||||
- Improve timeline UX
|
||||
- Fix bugs
|
||||
|
||||
## [v20240504](https://firefish.dev/firefish/firefish/-/merge_requests/10790/commits)
|
||||
|
||||
- Fix bugs
|
||||
|
|
|
@ -2301,3 +2301,6 @@ getQrCode: Mostrar el codi QR
|
|||
copyRemoteFollowUrl: Còpia la adreça URL del seguidor remot
|
||||
foldNotification: Agrupar les notificacions similars
|
||||
slashQuote: Cita encadenada
|
||||
i18nServerInfo: Els nous clients els trobares en {language} per defecte.
|
||||
i18nServerChange: Fes servir {language} en comptes.
|
||||
i18nServerSet: Fes servir {language} per els nous clients.
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { noteVisibilities } from "@/types.js";
|
||||
|
||||
export type Post = {
|
||||
text: string | undefined;
|
||||
cw: string | null;
|
||||
|
@ -12,7 +14,9 @@ export function parse(acct: any): Post {
|
|||
cw: acct.cw,
|
||||
localOnly: acct.localOnly,
|
||||
createdAt: new Date(acct.createdAt),
|
||||
visibility: `hidden${acct.visibility || ""}`,
|
||||
visibility: noteVisibilities.includes(acct.visibility)
|
||||
? acct.visibility
|
||||
: "public",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -83,22 +83,27 @@ export async function importCkPost(
|
|||
logger.info("Post updated");
|
||||
}
|
||||
if (note == null) {
|
||||
note = await create(user, {
|
||||
createdAt: createdAt,
|
||||
files: files.length === 0 ? undefined : files,
|
||||
poll: undefined,
|
||||
text: text || undefined,
|
||||
reply: post.replyId ? job.data.parent : null,
|
||||
renote: post.renoteId ? job.data.parent : null,
|
||||
cw: cw,
|
||||
localOnly,
|
||||
visibility: visibility,
|
||||
visibleUsers: [],
|
||||
channel: null,
|
||||
apMentions: new Array(0),
|
||||
apHashtags: undefined,
|
||||
apEmojis: undefined,
|
||||
});
|
||||
note = await create(
|
||||
user,
|
||||
{
|
||||
createdAt: createdAt,
|
||||
files: files.length === 0 ? undefined : files,
|
||||
poll: undefined,
|
||||
text: text || undefined,
|
||||
reply: post.replyId ? job.data.parent : null,
|
||||
renote: post.renoteId ? job.data.parent : null,
|
||||
cw: cw,
|
||||
localOnly,
|
||||
visibility: visibility,
|
||||
visibleUsers: [],
|
||||
channel: null,
|
||||
apMentions: new Array(0),
|
||||
apHashtags: undefined,
|
||||
apEmojis: undefined,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
);
|
||||
logger.debug("New post has been created");
|
||||
} else {
|
||||
logger.info("This post already exists");
|
||||
|
|
|
@ -109,24 +109,30 @@ export async function importMastoPost(
|
|||
logger.info("Post updated");
|
||||
}
|
||||
if (note == null) {
|
||||
note = await create(user, {
|
||||
createdAt: isRenote
|
||||
? new Date(post.published)
|
||||
: new Date(post.object.published),
|
||||
files: files.length === 0 ? undefined : files,
|
||||
poll: undefined,
|
||||
text: text || undefined,
|
||||
reply,
|
||||
renote,
|
||||
cw: !isRenote && post.object.sensitive ? post.object.summary : undefined,
|
||||
localOnly: false,
|
||||
visibility: "hiddenpublic",
|
||||
visibleUsers: [],
|
||||
channel: null,
|
||||
apMentions: new Array(0),
|
||||
apHashtags: undefined,
|
||||
apEmojis: undefined,
|
||||
});
|
||||
note = await create(
|
||||
user,
|
||||
{
|
||||
createdAt: isRenote
|
||||
? new Date(post.published)
|
||||
: new Date(post.object.published),
|
||||
files: files.length === 0 ? undefined : files,
|
||||
poll: undefined,
|
||||
text: text || undefined,
|
||||
reply,
|
||||
renote,
|
||||
cw:
|
||||
!isRenote && post.object.sensitive ? post.object.summary : undefined,
|
||||
localOnly: false,
|
||||
visibility: "public",
|
||||
visibleUsers: [],
|
||||
channel: null,
|
||||
apMentions: new Array(0),
|
||||
apHashtags: undefined,
|
||||
apEmojis: undefined,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
);
|
||||
logger.debug("New post has been created");
|
||||
} else {
|
||||
logger.info("This post already exists");
|
||||
|
|
|
@ -175,11 +175,12 @@ export default async (
|
|||
},
|
||||
data: Option,
|
||||
silent = false,
|
||||
dontFederateInitially = false,
|
||||
) =>
|
||||
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: FIXME
|
||||
new Promise<Note>(async (res, rej) => {
|
||||
const dontFederateInitially =
|
||||
data.visibility?.startsWith("hidden") === true;
|
||||
const dontFederateInitiallyFlag =
|
||||
data.visibility === "hidden" ? true : dontFederateInitially;
|
||||
|
||||
// If you reply outside the channel, match the scope of the target.
|
||||
// TODO (I think it's a process that could be done on the client side, but it's server side for now.)
|
||||
|
@ -213,8 +214,6 @@ export default async (
|
|||
if (data.channel != null) data.visibility = "public";
|
||||
if (data.channel != null) data.visibleUsers = [];
|
||||
if (data.channel != null) data.localOnly = true;
|
||||
if (data.visibility.startsWith("hidden") && data.visibility !== "hidden")
|
||||
data.visibility = data.visibility.slice(6);
|
||||
|
||||
// enforce silent clients on server
|
||||
if (
|
||||
|
@ -477,7 +476,7 @@ export default async (
|
|||
}
|
||||
}
|
||||
|
||||
if (!dontFederateInitially) {
|
||||
if (!dontFederateInitiallyFlag) {
|
||||
let publishKey: string;
|
||||
let noteToPublish: Note;
|
||||
const relays = await getCachedRelays();
|
||||
|
@ -615,7 +614,7 @@ export default async (
|
|||
});
|
||||
|
||||
//#region AP deliver
|
||||
if (Users.isLocalUser(user) && !dontFederateInitially) {
|
||||
if (Users.isLocalUser(user) && !dontFederateInitiallyFlag) {
|
||||
(async () => {
|
||||
const noteActivity = await renderNoteOrRenoteActivity(data, note);
|
||||
const dm = new DeliverManager(user, noteActivity);
|
||||
|
|
|
@ -18,8 +18,8 @@
|
|||
class="reply-to"
|
||||
/>
|
||||
<MkNoteSub
|
||||
v-else-if="!detailedView && !collapsedReply && parents"
|
||||
v-for="n of parents"
|
||||
v-else-if="!detailedView && !collapsedReply && parents"
|
||||
:key="n.id"
|
||||
:note="n"
|
||||
class="reply-to"
|
||||
|
@ -354,7 +354,7 @@ import { deepClone } from "@/scripts/clone";
|
|||
import { getNoteSummary } from "@/scripts/get-note-summary";
|
||||
import icon from "@/scripts/icon";
|
||||
import type { NoteTranslation, NoteType } from "@/types/note";
|
||||
import { isRenote as _isRenote, isDeleted as _isDeleted } from "@/scripts/note";
|
||||
import { isDeleted as _isDeleted, isRenote as _isRenote } from "@/scripts/note";
|
||||
|
||||
const props = defineProps<{
|
||||
note: NoteType;
|
||||
|
@ -368,7 +368,7 @@ const props = defineProps<{
|
|||
isLongJudger?: (note: entities.Note) => boolean;
|
||||
}>();
|
||||
|
||||
//#region Constants
|
||||
// #region Constants
|
||||
const router = useRouter();
|
||||
const inChannel = inject("inChannel", null);
|
||||
const keymap = {
|
||||
|
@ -398,9 +398,9 @@ const currentClipPage = inject<Ref<entities.Clip> | null>(
|
|||
"currentClipPage",
|
||||
null,
|
||||
);
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region Variables bound to Notes
|
||||
// #region Variables bound to Notes
|
||||
let capture: ReturnType<typeof useNoteCapture> | undefined;
|
||||
const note = ref(deepClone(props.note));
|
||||
const postIsExpanded = ref(false);
|
||||
|
@ -408,9 +408,9 @@ const translation = ref<NoteTranslation | null>(null);
|
|||
const translating = ref(false);
|
||||
const isDeleted = ref(false);
|
||||
const renotes = ref(props.renotes?.filter((rn) => !_isDeleted(rn.id)));
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
//#region computed
|
||||
// #region computed
|
||||
|
||||
const renotesSliced = computed(() => renotes.value?.slice(0, 5));
|
||||
|
||||
|
@ -470,7 +470,7 @@ const accessibleLabel = computed(() => {
|
|||
label += `${date.toLocaleTimeString()}`;
|
||||
return label;
|
||||
});
|
||||
//#endregion
|
||||
// #endregion
|
||||
|
||||
async function pluginInit(newNote: NoteType) {
|
||||
// plugin
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div
|
||||
v-show="!deleted"
|
||||
ref="el"
|
||||
v-size="{ min: [350, 500] }"
|
||||
class="yohlumlk"
|
||||
ref="el"
|
||||
>
|
||||
<MkAvatar class="avatar" :user="note.user" />
|
||||
<div class="main">
|
||||
|
@ -17,9 +17,9 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import type { entities } from "firefish-js";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import XNoteHeader from "@/components/MkNoteHeader.vue";
|
||||
import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
|
||||
import { computed, ref, watch } from "vue";
|
||||
import { deepClone } from "@/scripts/clone";
|
||||
import { useNoteCapture } from "@/scripts/use-note-capture";
|
||||
import { isDeleted } from "@/scripts/note";
|
||||
|
|
|
@ -28,9 +28,9 @@
|
|||
ref="tlComponent"
|
||||
:no-gap="!defaultStore.state.showGapBetweenNotesInTimeline"
|
||||
:pagination="pagination"
|
||||
:folder
|
||||
@queue="(x) => (queue = x)"
|
||||
@status="pullToRefreshComponent?.setDisabled($event)"
|
||||
:folder
|
||||
/>
|
||||
</MkPullToRefresh>
|
||||
<XNotes
|
||||
|
@ -38,9 +38,9 @@
|
|||
ref="tlComponent"
|
||||
:no-gap="!defaultStore.state.showGapBetweenNotesInTimeline"
|
||||
:pagination="pagination"
|
||||
:folder
|
||||
@queue="(x) => (queue = x)"
|
||||
@status="pullToRefreshComponent?.setDisabled($event)"
|
||||
:folder
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -25,15 +25,21 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
function getDateSafe(n: Date | string | number) {
|
||||
try {
|
||||
if (n instanceof Date) {
|
||||
return n;
|
||||
}
|
||||
return new Date(n);
|
||||
} catch (err) {
|
||||
return {
|
||||
getTime: () => Number.NaN,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const _time = computed(() =>
|
||||
props.time == null
|
||||
? Number.NaN
|
||||
: typeof props.time === "number"
|
||||
? props.time
|
||||
: (props.time instanceof Date
|
||||
? props.time
|
||||
: new Date(props.time)
|
||||
).getTime(),
|
||||
props.time == null ? Number.NaN : getDateSafe(props.time).getTime(),
|
||||
);
|
||||
const invalid = computed(() => Number.isNaN(_time.value));
|
||||
const absolute = computed(() =>
|
||||
|
@ -41,45 +47,57 @@ const absolute = computed(() =>
|
|||
);
|
||||
|
||||
const now = ref(props.origin?.getTime() ?? Date.now());
|
||||
|
||||
const relative = computed<string>(() => {
|
||||
if (props.mode === "absolute") return ""; // absoluteではrelativeを使わないので計算しない
|
||||
if (invalid.value) return i18n.ts._ago.invalid;
|
||||
|
||||
const ago = (now.value - _time.value) / 1000; /* ms */
|
||||
return ago >= 31536000
|
||||
? i18n.t("_ago.yearsAgo", { n: Math.floor(ago / 31536000).toString() })
|
||||
: ago >= 2592000
|
||||
? i18n.t("_ago.monthsAgo", {
|
||||
n: Math.floor(ago / 2592000).toString(),
|
||||
})
|
||||
: ago >= 604800
|
||||
? i18n.t("_ago.weeksAgo", {
|
||||
n: Math.floor(ago / 604800).toString(),
|
||||
})
|
||||
: ago >= 86400
|
||||
? i18n.t("_ago.daysAgo", {
|
||||
n: Math.floor(ago / 86400).toString(),
|
||||
})
|
||||
: ago >= 3600
|
||||
? i18n.t("_ago.hoursAgo", {
|
||||
n: Math.floor(ago / 3600).toString(),
|
||||
})
|
||||
: ago >= 60
|
||||
? i18n.t("_ago.minutesAgo", {
|
||||
n: (~~(ago / 60)).toString(),
|
||||
})
|
||||
: ago >= 10
|
||||
? i18n.t("_ago.secondsAgo", {
|
||||
n: (~~(ago % 60)).toString(),
|
||||
})
|
||||
: ago >= -1
|
||||
? i18n.ts._ago.justNow
|
||||
: i18n.ts._ago.future;
|
||||
|
||||
if (ago >= 31536000) {
|
||||
return i18n.t("_ago.yearsAgo", {
|
||||
n: Math.floor(ago / 31536000).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 2592000) {
|
||||
return i18n.t("_ago.monthsAgo", {
|
||||
n: Math.floor(ago / 2592000).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 604800) {
|
||||
return i18n.t("_ago.weeksAgo", {
|
||||
n: Math.floor(ago / 604800).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 86400) {
|
||||
return i18n.t("_ago.daysAgo", {
|
||||
n: Math.floor(ago / 86400).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 3600) {
|
||||
return i18n.t("_ago.hoursAgo", {
|
||||
n: Math.floor(ago / 3600).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 60) {
|
||||
return i18n.t("_ago.minutesAgo", {
|
||||
n: (~~(ago / 60)).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= 10) {
|
||||
return i18n.t("_ago.secondsAgo", {
|
||||
n: (~~(ago % 60)).toString(),
|
||||
});
|
||||
}
|
||||
if (ago >= -1) {
|
||||
return i18n.ts._ago.justNow;
|
||||
}
|
||||
return i18n.ts._ago.future;
|
||||
});
|
||||
|
||||
let tickId: number | undefined;
|
||||
|
||||
function tick() {
|
||||
function tick(forceUpdateTicker = false) {
|
||||
if (
|
||||
invalid.value ||
|
||||
props.origin ||
|
||||
|
@ -101,13 +119,16 @@ function tick() {
|
|||
|
||||
if (!tickId) {
|
||||
tickId = window.setInterval(tick, next);
|
||||
} else if (prev < next) {
|
||||
} else if (prev < next || forceUpdateTicker) {
|
||||
window.clearInterval(tickId);
|
||||
tickId = window.setInterval(tick, next);
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.time, tick);
|
||||
watch(
|
||||
() => props.time,
|
||||
() => tick(true),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
tick();
|
||||
|
|
|
@ -14,10 +14,10 @@
|
|||
>
|
||||
</template>
|
||||
</I18n>
|
||||
<I18n :src="i18n.ts.i18nServerInfo" v-if="serverLang" tag="span">
|
||||
<I18n v-if="serverLang" :src="i18n.ts.i18nServerInfo" tag="span">
|
||||
<template #language><strong>{{ langs.find(a => a[0] === serverLang)?.[1] ?? serverLang }}</strong></template>
|
||||
</I18n>
|
||||
<button class="_textButton" @click="updateServerLang" v-if="lang && lang !== serverLang">
|
||||
<button v-if="lang && lang !== serverLang" class="_textButton" @click="updateServerLang">
|
||||
{{i18n.t(serverLang ? "i18nServerChange" : "i18nServerSet", { language: langs.find(a => a[0] === lang)?.[1] ?? lang })}}
|
||||
</button>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import type { entities } from "firefish-js";
|
||||
import { isDeleted, isRenote } from "./note";
|
||||
import type {
|
||||
FoldableNotification,
|
||||
NotificationFolded,
|
||||
} from "@/types/notification";
|
||||
import type { NoteType, NoteThread, NoteFolded } from "@/types/note";
|
||||
import type { NoteFolded, NoteThread, NoteType } from "@/types/note";
|
||||
import { me } from "@/me";
|
||||
import { isDeleted, isRenote } from "./note";
|
||||
|
||||
interface FoldOption {
|
||||
/** If items length is 1, skip aggregation */
|
||||
|
|
|
@ -12,19 +12,19 @@ export type NoteType = entities.Note & {
|
|||
_prId_?: string;
|
||||
};
|
||||
|
||||
export type NoteFolded = {
|
||||
export interface NoteFolded {
|
||||
id: string;
|
||||
key: string;
|
||||
createdAt: entities.Note["createdAt"];
|
||||
folded: "renote";
|
||||
note: entities.Note;
|
||||
renotesArr: entities.Note[];
|
||||
};
|
||||
}
|
||||
|
||||
export type NoteThread = {
|
||||
export interface NoteThread {
|
||||
id: string;
|
||||
createdAt: entities.Note["createdAt"];
|
||||
folded: "thread";
|
||||
note: entities.Note;
|
||||
parents: entities.Note[];
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue