Compare commits
21 Commits
04ad90563d
...
1ec8fb6038
Author | SHA1 | Date |
---|---|---|
laozhoubuluo | 1ec8fb6038 | |
naskya | 3af8f86924 | |
naskya | 276cabbbe3 | |
naskya | af14bee31f | |
naskya | b3d1be457b | |
Hosted Weblate | 347851d6bb | |
jolupa | abec71074b | |
Lhcfl | 272e30be0c | |
naskya | 971f196627 | |
naskya | 8cc0e40d35 | |
naskya | beeea86253 | |
naskya | 084a4bc63a | |
naskya | cda31d3dc7 | |
naskya | 907578e8f8 | |
naskya | 2923ea86de | |
naskya | 226c990385 | |
naskya | 769f52c8ee | |
naskya | 8a00d82f36 | |
naskya | 34ed877f57 | |
Lhcfl | f5074f35cc | |
老周部落 | 66a7bf2283 |
|
@ -3,8 +3,10 @@ image: docker.io/rust:slim-bookworm
|
|||
services:
|
||||
- name: docker.io/groonga/pgroonga:latest-alpine-12-slim
|
||||
alias: postgres
|
||||
pull_policy: if-not-present
|
||||
- name: docker.io/redis:7-alpine
|
||||
alias: redis
|
||||
pull_policy: if-not-present
|
||||
|
||||
workflow:
|
||||
rules:
|
||||
|
@ -12,6 +14,11 @@ workflow:
|
|||
when: always
|
||||
- if: $CI_MERGE_REQUEST_PROJECT_PATH == 'firefish/firefish'
|
||||
when: always
|
||||
- if: $CI_PROJECT_PATH != 'firefish/firefish'
|
||||
changes:
|
||||
paths:
|
||||
- .gitlab-ci.yml
|
||||
when: never
|
||||
- when: never
|
||||
|
||||
cache:
|
||||
|
@ -56,9 +63,11 @@ build_test:
|
|||
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/**/*
|
||||
- packages/backend/*
|
||||
- packages/backend-rs/*
|
||||
- packages/macro-rs/*
|
||||
- packages/megalodon/*
|
||||
- scripts/**/*
|
||||
- locales/**/*
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- Cargo.toml
|
||||
|
@ -68,6 +77,39 @@ build_test:
|
|||
- pnpm run build:debug
|
||||
- pnpm run migrate
|
||||
|
||||
client_build_test:
|
||||
stage: test
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/client/*
|
||||
- packages/firefish-js/*
|
||||
- packages/sw/*
|
||||
- locales/**/*
|
||||
- if: $CI_PIPELINE_SOURCE == 'push' || $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||
changes:
|
||||
paths:
|
||||
- packages/backend/*
|
||||
- packages/backend-rs/*
|
||||
- packages/macro-rs/*
|
||||
- packages/megalodon/*
|
||||
- Cargo.toml
|
||||
- Cargo.lock
|
||||
when: never
|
||||
services: []
|
||||
before_script:
|
||||
- apt-get update && apt-get -y upgrade
|
||||
- apt-get -y --no-install-recommends install curl
|
||||
- curl -fsSL 'https://deb.nodesource.com/setup_18.x' | bash -
|
||||
- apt-get install -y --no-install-recommends build-essential python3 perl nodejs
|
||||
- corepack enable
|
||||
- corepack prepare pnpm@latest --activate
|
||||
- cp .config/ci.yml .config/default.yml
|
||||
script:
|
||||
- pnpm install --frozen-lockfile
|
||||
- pnpm --filter 'firefish-js' --filter 'client' --filter 'sw' run build:debug
|
||||
|
||||
container_image_build:
|
||||
stage: build
|
||||
image: docker.io/debian:bookworm-slim
|
||||
|
@ -90,8 +132,21 @@ container_image_build:
|
|||
- apt-get install -y --no-install-recommends buildah ca-certificates fuse-overlayfs
|
||||
- buildah login --username "${CI_REGISTRY_USER}" --password "${CI_REGISTRY_PASSWORD}" "${CI_REGISTRY}"
|
||||
- export IMAGE_TAG="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop:not-for-production"
|
||||
- export IMAGE_CACHE="${CI_REGISTRY}/${CI_PROJECT_PATH}/develop/cache"
|
||||
script:
|
||||
- buildah build --isolation chroot --device /dev/fuse:rw --security-opt seccomp=unconfined --security-opt apparmor=unconfined --cap-add all --tag "${IMAGE_TAG}" --platform linux/amd64 .
|
||||
- |-
|
||||
buildah build \
|
||||
--isolation chroot \
|
||||
--device /dev/fuse:rw \
|
||||
--security-opt seccomp=unconfined \
|
||||
--security-opt apparmor=unconfined \
|
||||
--cap-add all \
|
||||
--platform linux/amd64 \
|
||||
--layers \
|
||||
--cache-to "${IMAGE_CACHE}" \
|
||||
--cache-from "${IMAGE_CACHE}" \
|
||||
--tag "${IMAGE_TAG}" \
|
||||
.
|
||||
- buildah inspect "${IMAGE_TAG}"
|
||||
- buildah push "${IMAGE_TAG}"
|
||||
|
||||
|
@ -128,8 +183,13 @@ cargo_clippy:
|
|||
- Cargo.lock
|
||||
- if: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == 'main'
|
||||
when: never
|
||||
script:
|
||||
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
|
||||
script:
|
||||
- cargo clippy -- -D warnings
|
||||
|
||||
renovate:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1292,7 +1292,6 @@ export interface AbuseUserReportLike {
|
|||
comment: string
|
||||
}
|
||||
export function publishToModerationStream(moderatorId: string, report: AbuseUserReportLike): void
|
||||
export function publishToNotesStream(note: Note): void
|
||||
export function getTimestamp(id: string): number
|
||||
/**
|
||||
* The generated ID results in the form of `[8 chars timestamp] + [cuid2]`.
|
||||
|
|
|
@ -310,7 +310,7 @@ if (!nativeBinding) {
|
|||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, publishToNotesStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||
const { SECOND, MINUTE, HOUR, DAY, USER_ONLINE_THRESHOLD, USER_ACTIVE_THRESHOLD, FILE_TYPE_BROWSERSAFE, loadEnv, loadConfig, stringToAcct, acctToString, addNoteToAntenna, isBlockedServer, isSilencedServer, isAllowedServer, checkWordMute, getFullApAccount, isSelfHost, isSameOrigin, extractHost, toPuny, isUnicodeEmoji, sqlLikeEscape, safeForSql, formatMilliseconds, getImageSizeFromUrl, getNoteSummary, isSafeUrl, latestVersion, toMastodonId, fromMastodonId, fetchMeta, metaToPugArgs, nyaify, hashPassword, verifyPassword, isOldPasswordAlgorithm, decodeReaction, countReactions, toDbReaction, removeOldAttestationChallenges, AntennaSrcEnum, DriveFileUsageHintEnum, MutedNoteReasonEnum, NoteVisibilityEnum, NotificationTypeEnum, PageVisibilityEnum, PollNotevisibilityEnum, RelayStatusEnum, UserEmojimodpermEnum, UserProfileFfvisibilityEnum, UserProfileMutingnotificationtypesEnum, initializeRustLogger, fetchNodeinfo, nodeinfo_2_1, nodeinfo_2_0, Protocol, Inbound, Outbound, watchNote, unwatchNote, publishToChannelStream, ChatEvent, publishToChatStream, ChatIndexEvent, publishToChatIndexStream, publishToBroadcastStream, publishToGroupChatStream, publishToModerationStream, getTimestamp, genId, genIdAt, secureRndstr } = nativeBinding
|
||||
|
||||
module.exports.SECOND = SECOND
|
||||
module.exports.MINUTE = MINUTE
|
||||
|
@ -381,7 +381,6 @@ module.exports.publishToChatIndexStream = publishToChatIndexStream
|
|||
module.exports.publishToBroadcastStream = publishToBroadcastStream
|
||||
module.exports.publishToGroupChatStream = publishToGroupChatStream
|
||||
module.exports.publishToModerationStream = publishToModerationStream
|
||||
module.exports.publishToNotesStream = publishToNotesStream
|
||||
module.exports.getTimestamp = getTimestamp
|
||||
module.exports.genId = genId
|
||||
module.exports.genIdAt = genIdAt
|
||||
|
|
|
@ -5,7 +5,6 @@ pub mod chat_index;
|
|||
pub mod custom_emoji;
|
||||
pub mod group_chat;
|
||||
pub mod moderation;
|
||||
pub mod new_note;
|
||||
|
||||
use crate::config::CONFIG;
|
||||
use crate::database::redis_conn;
|
||||
|
@ -26,7 +25,7 @@ pub enum Stream {
|
|||
#[strum(to_string = "noteStream:{note_id}")]
|
||||
Note { note_id: String },
|
||||
#[strum(serialize = "notesStream")]
|
||||
NewNote,
|
||||
Notes,
|
||||
#[strum(to_string = "userListStream:{list_id}")]
|
||||
UserList { list_id: String },
|
||||
#[strum(to_string = "mainStream:{user_id}")]
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
use crate::model::entity::note;
|
||||
use crate::service::stream::{publish_to_stream, Error, Stream};
|
||||
|
||||
// for napi export (https://github.com/napi-rs/napi-rs/issues/2060)
|
||||
type Note = note::Model;
|
||||
|
||||
#[crate::export(js_name = "publishToNotesStream")]
|
||||
pub fn publish(note: &Note) -> Result<(), Error> {
|
||||
publish_to_stream(&Stream::NewNote, None, Some(serde_json::to_string(note)?))
|
||||
}
|
|
@ -15,6 +15,23 @@ import { isMimeImage } from "@/misc/is-mime-image.js";
|
|||
import { inspect } from "node:util";
|
||||
|
||||
export async function proxyMedia(ctx: Koa.Context) {
|
||||
try {
|
||||
proxyMediaInternal(ctx);
|
||||
} catch (e) {
|
||||
if (ctx.query.fallback) {
|
||||
ctx.redirect("/static-assets/badges/error.webp");
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
if (ctx.status !== 200 && ctx.query.fallback) {
|
||||
ctx.redirect("/static-assets/badges/error.webp");
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
async function proxyMediaInternal(ctx: Koa.Context) {
|
||||
let url = "url" in ctx.query ? ctx.query.url : `https://${ctx.params.url}`;
|
||||
|
||||
if (typeof url !== "string") {
|
||||
|
@ -24,9 +41,10 @@ export async function proxyMedia(ctx: Koa.Context) {
|
|||
|
||||
url = url.replace("//", "/");
|
||||
|
||||
const { hostname } = new URL(url);
|
||||
let hostname;
|
||||
let resolvedIps;
|
||||
try {
|
||||
hostname = new URL(url);
|
||||
resolvedIps = await promises.resolve(hostname);
|
||||
} catch (error) {
|
||||
ctx.status = 400;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import * as mfm from "mfm-js";
|
||||
import { publishMainStream, publishNoteStream } from "@/services/stream.js";
|
||||
import {
|
||||
publishMainStream,
|
||||
publishNotesStream,
|
||||
publishNoteStream,
|
||||
} from "@/services/stream.js";
|
||||
import DeliverManager from "@/remote/activitypub/deliver-manager.js";
|
||||
import renderNote from "@/remote/activitypub/renderer/note.js";
|
||||
import renderCreate from "@/remote/activitypub/renderer/create.js";
|
||||
|
@ -45,7 +49,6 @@ import {
|
|||
genId,
|
||||
genIdAt,
|
||||
isSilencedServer,
|
||||
publishToNotesStream,
|
||||
} from "backend-rs";
|
||||
import { countSameRenotes } from "@/misc/count-same-renotes.js";
|
||||
import { deliverToRelays, getCachedRelays } from "../relay.js";
|
||||
|
@ -508,7 +511,7 @@ export default async (
|
|||
30,
|
||||
);
|
||||
}
|
||||
publishToNotesStream(toRustObject(noteToPublish));
|
||||
publishNotesStream(noteToPublish);
|
||||
}
|
||||
} finally {
|
||||
await lock.release();
|
||||
|
|
|
@ -193,10 +193,9 @@ class Publisher {
|
|||
// );
|
||||
// };
|
||||
|
||||
/* ported to backend-rs */
|
||||
// public publishNotesStream = (note: Note): void => {
|
||||
// this.publish("notesStream", null, note);
|
||||
// };
|
||||
public publishNotesStream = (note: Note): void => {
|
||||
this.publish("notesStream", null, note);
|
||||
};
|
||||
|
||||
/* ported to backend-rs */
|
||||
// public publishAdminStream = <K extends keyof AdminStreamTypes>(
|
||||
|
@ -222,7 +221,7 @@ export const publishUserEvent = publisher.publishUserEvent;
|
|||
export const publishMainStream = publisher.publishMainStream;
|
||||
export const publishDriveStream = publisher.publishDriveStream;
|
||||
export const publishNoteStream = publisher.publishNoteStream;
|
||||
// export const publishNotesStream = publisher.publishNotesStream;
|
||||
export const publishNotesStream = publisher.publishNotesStream;
|
||||
// export const publishChannelStream = publisher.publishChannelStream;
|
||||
export const publishUserListStream = publisher.publishUserListStream;
|
||||
// export const publishAntennaStream = publisher.publishAntennaStream;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -17,7 +17,11 @@
|
|||
}"
|
||||
>
|
||||
<div v-if="thumbnail" class="thumbnail">
|
||||
<img :src="thumbnail" loading="lazy" />
|
||||
<img
|
||||
:src="thumbnail"
|
||||
loading="lazy"
|
||||
@error="imageErrorEvent($event)"
|
||||
/>
|
||||
<button
|
||||
v-if="tweetId"
|
||||
v-tooltip="
|
||||
|
@ -55,7 +59,12 @@
|
|||
<p :title="description">
|
||||
<span>
|
||||
<span :title="sitename || undefined">
|
||||
<img v-if="icon" class="icon" :src="icon" />
|
||||
<img
|
||||
v-if="icon"
|
||||
class="icon"
|
||||
:src="icon"
|
||||
@error="imageErrorEvent($event)"
|
||||
/>
|
||||
{{ sitename }}
|
||||
</span>
|
||||
{{ description }}
|
||||
|
@ -181,6 +190,10 @@ function adjustTweetHeight(message: any) {
|
|||
if (height) tweetHeight.value = height;
|
||||
}
|
||||
|
||||
function imageErrorEvent($event) {
|
||||
$event.target.src = "/static-assets/badges/error.webp";
|
||||
}
|
||||
|
||||
window.addEventListener("message", adjustTweetHeight);
|
||||
|
||||
onUnmounted(() => {
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
|
||||
import { i18n } from "@/i18n";
|
||||
import { dateTimeFormat } from "@/scripts/intl-const";
|
||||
|
||||
|
@ -25,85 +25,116 @@ const props = withDefaults(
|
|||
},
|
||||
);
|
||||
|
||||
const _time =
|
||||
props.time == null
|
||||
? Number.NaN
|
||||
: typeof props.time === "number"
|
||||
? props.time
|
||||
: (props.time instanceof Date
|
||||
? props.time
|
||||
: new Date(props.time)
|
||||
).getTime();
|
||||
const invalid = Number.isNaN(_time);
|
||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||
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 : getDateSafe(props.time).getTime(),
|
||||
);
|
||||
const invalid = computed(() => Number.isNaN(_time.value));
|
||||
const absolute = computed(() =>
|
||||
!invalid.value ? dateTimeFormat.format(_time.value) : i18n.ts._ago.invalid,
|
||||
);
|
||||
|
||||
const now = ref(props.origin?.getTime() ?? Date.now());
|
||||
|
||||
const relative = computed<string>(() => {
|
||||
if (props.mode === "absolute") return ""; // absoluteではrelativeを使わないので計算しない
|
||||
if (invalid) return i18n.ts._ago.invalid;
|
||||
if (invalid.value) return i18n.ts._ago.invalid;
|
||||
|
||||
const ago = (now.value - _time) / 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;
|
||||
const ago = (now.value - _time.value) / 1000; /* ms */
|
||||
|
||||
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;
|
||||
let tickId: number | undefined;
|
||||
|
||||
function tick(forceUpdateTicker = false) {
|
||||
if (
|
||||
invalid.value ||
|
||||
props.origin ||
|
||||
(props.mode !== "relative" && props.mode !== "detail")
|
||||
) {
|
||||
if (tickId) window.clearInterval(tickId);
|
||||
tickId = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
function tick() {
|
||||
const _now = Date.now();
|
||||
const agoPrev = (now.value - _time) / 1000; /* ms */ // 現状のinterval
|
||||
const agoPrev = (now.value - _time.value) / 1000; /* ms */ // 現状のinterval
|
||||
|
||||
now.value = _now;
|
||||
|
||||
const ago = (now.value - _time) / 1000; /* ms */ // 次のinterval
|
||||
const ago = (now.value - _time.value) / 1000; /* ms */ // 次のinterval
|
||||
const prev = agoPrev < 60 ? 10000 : agoPrev < 3600 ? 60000 : 180000;
|
||||
const next = ago < 60 ? 10000 : ago < 3600 ? 60000 : 180000;
|
||||
|
||||
if (!tickId) {
|
||||
tickId = window.setInterval(tick, next);
|
||||
} else if (prev < next) {
|
||||
} else if (prev < next || forceUpdateTicker) {
|
||||
window.clearInterval(tickId);
|
||||
tickId = window.setInterval(tick, next);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!invalid &&
|
||||
!props.origin &&
|
||||
(props.mode === "relative" || props.mode === "detail")
|
||||
) {
|
||||
onMounted(() => {
|
||||
tick();
|
||||
});
|
||||
onUnmounted(() => {
|
||||
if (tickId) window.clearInterval(tickId);
|
||||
});
|
||||
}
|
||||
watch(
|
||||
() => props.time,
|
||||
() => tick(true),
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
tick();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (tickId) window.clearInterval(tickId);
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -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