Compare commits

...

3 Commits

Author SHA1 Message Date
Natty b74f2d69a4
Reintroduced graceful shutdown
ci/woodpecker/push/ociImagePush Pipeline is running Details
2024-05-20 17:23:08 +02:00
Natty e06906dd6e
Also fetch profile in cache 2024-04-30 16:02:35 +02:00
Natty 155e458806
Frontend: Code and post form cleanup 2024-04-30 13:52:13 +02:00
25 changed files with 821 additions and 721 deletions

View File

@ -1,3 +1,34 @@
use std::future::Future;
use chrono::Utc;
use futures_util::{SinkExt, StreamExt};
use redis::IntoConnectionInfo;
pub use sea_orm;
use sea_orm::{ActiveValue::Set, ConnectionTrait};
use sea_orm::{
ColumnTrait, ConnectOptions, DatabaseConnection, DbErr, EntityTrait, QueryFilter,
TransactionTrait,
};
use serde::{Deserialize, Deserializer, Serialize};
use serde::de::Error;
use serde_json::Value;
use strum::IntoStaticStr;
use thiserror::Error;
use tokio::select;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, trace, warn};
use tracing::log::LevelFilter;
use url::Host;
pub use ck;
use ck::*;
use ext_model_migration::{Migrator, MigratorTrait};
use user_model::UserResolver;
use crate::model_ext::IdShape;
use crate::note_model::NoteResolver;
use crate::notification_model::NotificationResolver;
pub mod emoji;
pub mod model_ext;
pub mod note_model;
@ -5,35 +36,6 @@ pub mod notification_model;
pub mod poll;
pub mod user_model;
pub use ck;
use ck::*;
pub use sea_orm;
use url::Host;
use user_model::UserResolver;
use crate::model_ext::IdShape;
use crate::note_model::NoteResolver;
use crate::notification_model::NotificationResolver;
use chrono::Utc;
use ext_model_migration::{Migrator, MigratorTrait};
use futures_util::StreamExt;
use redis::IntoConnectionInfo;
use sea_orm::{ActiveValue::Set, ConnectionTrait};
use sea_orm::{
ColumnTrait, ConnectOptions, DatabaseConnection, DbErr, EntityTrait, QueryFilter,
TransactionTrait,
};
use serde::de::Error;
use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::future::Future;
use strum::IntoStaticStr;
use thiserror::Error;
use tokio::select;
use tokio_util::sync::CancellationToken;
use tracing::log::LevelFilter;
use tracing::{error, info, trace, warn};
#[derive(Debug)]
pub struct ConnectorConfig {
pub url: String,
@ -120,6 +122,15 @@ impl CalckeyModel {
.await?)
}
pub async fn get_user_and_profile_by_id(&self, id: &str) -> Result<Option<(user::Model, user_profile::Model)>, CalckeyDbError> {
Ok(user::Entity::find()
.filter(user::Column::Id.eq(id))
.find_also_related(user_profile::Entity)
.one(&self.0)
.await?
.and_then(|(u, p)| p.map(|pp| (u, pp))))
}
pub async fn get_user_security_keys_by_id(
&self,
id: &str,
@ -154,6 +165,22 @@ impl CalckeyModel {
.await?)
}
pub async fn get_user_and_profile_by_token(
&self,
token: &str,
) -> Result<Option<(user::Model, user_profile::Model)>, CalckeyDbError> {
Ok(user::Entity::find()
.filter(
user::Column::Token
.eq(token)
.and(user::Column::Host.is_null()),
)
.find_also_related(user_profile::Entity)
.one(&self.0)
.await?
.and_then(|(u, p)| p.map(|pp| (u, pp))))
}
pub async fn get_user_by_uri(&self, uri: &str) -> Result<Option<user::Model>, CalckeyDbError> {
Ok(user::Entity::find()
.filter(user::Column::Uri.eq(uri))
@ -439,7 +466,7 @@ pub enum InternalStreamMessage {
}
impl CalckeyCacheClient {
pub async fn subscribe<F: Future<Output = ()> + Send + 'static>(
pub async fn subscribe<F: Future<Output=()> + Send + 'static>(
self,
prefix: &str,
handler: impl Fn(SubMessage) -> F + Send + Sync + 'static,
@ -451,7 +478,7 @@ impl CalckeyCacheClient {
pub struct CalckeySub(CancellationToken);
impl CalckeySub {
async fn new<F: Future<Output = ()> + Send + 'static>(
async fn new<F: Future<Output=()> + Send + 'static>(
conn: redis::aio::Connection,
prefix: &str,
handler: impl Fn(SubMessage) -> F + Send + Sync + 'static,

View File

@ -5,7 +5,6 @@ import type {
MeDetailed,
Note,
Notification,
PageEvent,
User,
} from "./entities";
@ -23,7 +22,6 @@ export type Channels = {
followed: (payload: User) => void; // 他人が自分をフォローしたとき
unfollow: (payload: User) => void; // 自分が他人をフォロー解除したとき
meUpdated: (payload: MeDetailed) => void;
pageEvent: (payload: PageEvent) => void;
urlUploadFinished: (payload: { marker: string; file: DriveFile }) => void;
readAllNotifications: () => void;
unreadNotification: (payload: Notification) => void;

View File

@ -64,7 +64,7 @@
"prismjs": "1.29.0",
"punycode": "2.1.1",
"rndstr": "1.0.0",
"rollup": "^4.17.1",
"rollup": "^4.17.2",
"s-age": "1.1.2",
"sass": "1.62.1",
"seedrandom": "3.0.5",
@ -88,7 +88,7 @@
"vite": "^5.2.10",
"vite-plugin-compression": "^0.5.1",
"vue": "^3.4.26",
"vue-component-type-helpers": "^2.0.14",
"vue-component-type-helpers": "^2.0.15",
"vue-isyourpasswordsafe": "^2.0.0",
"vue3-otp-input": "^0.4.4",
"vuedraggable": "4.1.0"

View File

@ -3,7 +3,7 @@
:is="self ? 'MkA' : 'a'"
ref="el"
class="xlcxczvw _link"
:[attr]="self ? url.substr(local.length) : url"
:[attr]="self ? url.substring(local.length) : url"
:rel="rel"
:target="target"
:title="url"
@ -28,7 +28,7 @@ const props = withDefaults(
url: string;
rel?: null | string;
}>(),
{},
{}
);
const self = props.url.startsWith(local);
@ -40,7 +40,7 @@ const el = ref();
useTooltip(el, (showing) => {
os.popup(
defineAsyncComponent(
() => import("@/components/MkUrlPreviewPopup.vue"),
() => import("@/components/MkUrlPreviewPopup.vue")
),
{
showing,
@ -48,7 +48,7 @@ useTooltip(el, (showing) => {
source: el.value,
},
{},
"closed",
"closed"
);
});
</script>

View File

@ -10,7 +10,7 @@
<script lang="ts" setup>
import { toUnicode } from "punycode";
import MkLink from "@/components/MkLink.vue";
import MkLink from "@/components/MagLink.vue";
import { computed } from "vue";
const props = defineProps<{
@ -19,7 +19,7 @@ const props = defineProps<{
}>();
const url = computed(
() => `https://matrix.to/#/@${props.username}:${props.host}`,
() => `https://matrix.to/#/@${props.username}:${props.host}`
);
</script>

View File

@ -228,7 +228,7 @@ import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XQuoteButton from "@/components/MagQuoteButton.vue";
import MkVisibility from "@/components/MkVisibility.vue";
import MkVisibility from "@/components/MagVisibility.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config";
import { pleaseLogin } from "@/scripts/please-login";
@ -611,15 +611,18 @@ defineExpose({
padding: 0 32px 0 32px;
display: flex;
z-index: 1;
&:first-child {
margin-top: 20px;
}
> :not(.line) {
width: 0;
flex-grow: 1;
position: relative;
line-height: 28px;
}
> .line {
position: relative;
z-index: 2;
@ -634,6 +637,7 @@ defineExpose({
> div > i {
margin-left: -0.5px;
}
> .info {
display: flex;
align-items: center;
@ -683,6 +687,7 @@ defineExpose({
color: inherit;
display: inline-flex;
align-items: center;
> .dropdownIcon {
margin-right: 4px;
}
@ -693,6 +698,7 @@ defineExpose({
&.collapsedReply {
.line {
opacity: 0.25;
&::after {
content: "";
position: absolute;
@ -705,10 +711,12 @@ defineExpose({
height: calc(50% + 5px);
}
}
.info {
color: var(--fgTransparentWeak);
transition: color 0.2s;
}
.avatar {
width: 1.2em;
height: 1.2em;
@ -717,14 +725,17 @@ defineExpose({
margin-right: 0.4em;
background: var(--panelHighlight);
}
.username {
font-weight: 700;
flex-shrink: 0;
max-width: 30%;
&::after {
content: ": ";
}
}
&:hover,
&:focus-within {
.info {
@ -754,6 +765,7 @@ defineExpose({
display: flex;
position: relative;
z-index: 2;
> .avatar {
flex-shrink: 0;
display: block;
@ -764,30 +776,36 @@ defineExpose({
top: 0;
left: 0;
}
> .header {
width: 0;
flex-grow: 1;
}
}
> .main {
flex: 1;
min-width: 0;
> .body {
margin-top: 0.7em;
> .translation {
border: solid 0.5px var(--divider);
border-radius: var(--radius);
padding: 12px;
margin-top: 8px;
}
> .renote {
padding-top: 8px;
> * {
padding: 16px;
border: solid 1px var(--renote);
border-radius: 8px;
transition: background 0.2s;
&:hover,
&:focus-within {
background-color: var(--panelHighlight);
@ -795,6 +813,7 @@ defineExpose({
}
}
}
> .info {
display: flex;
justify-content: space-between;
@ -804,6 +823,7 @@ defineExpose({
opacity: 0.7;
font-size: 0.9em;
}
> .footer {
position: relative;
z-index: 2;
@ -811,6 +831,7 @@ defineExpose({
flex-wrap: wrap;
pointer-events: none; // Allow clicking anything w/out pointer-events: all; to open post
margin-top: 0.4em;
> :deep(.button) {
position: relative;
margin: 0;
@ -823,6 +844,7 @@ defineExpose({
pointer-events: all;
height: auto;
transition: opacity 0.2s;
&::before {
content: "";
position: absolute;
@ -832,17 +854,21 @@ defineExpose({
z-index: -1;
transition: background 0.2s;
}
&:first-of-type {
margin-left: -0.5em;
&::before {
border-radius: 100px 0 0 100px;
}
}
&:last-of-type {
&::before {
border-radius: 0 100px 100px 0;
}
}
&:hover {
color: var(--fgHighlighted);
}
@ -873,25 +899,32 @@ defineExpose({
font-size: 0.975em;
--avatarSize: 46px;
padding-top: 6px;
> .note-context {
padding-inline: 16px;
margin-top: 8px;
> :not(.line) {
margin-top: 0px;
}
> .line {
margin-right: 10px;
&::before {
margin-top: 8px;
}
}
}
> .article {
padding: 18px 16px 8px;
&:first-child,
&:nth-child(2) {
padding-top: 104px;
}
> .main > .header-container > .avatar {
margin-right: 10px;
// top: calc(14px + var(--stickyTop, 0px));

View File

@ -12,7 +12,9 @@
<span v-if="note.user.is_bot" class="is-bot">bot</span>
</MkUserName>
</MkA>
<div class="username"><MkAcct :user="note.user" /></div>
<div class="username">
<MkAcct :user="note.user" />
</div>
</div>
<div>
<div class="info">
@ -23,10 +25,10 @@
v-tooltip.noDelay="
i18n.t('edited', {
date: new Date(
note.updated_at,
note.updated_at
).toLocaleDateString(),
time: new Date(
note.updated_at,
note.updated_at
).toLocaleTimeString(),
})
"
@ -48,7 +50,7 @@
<script lang="ts" setup>
import { defaultStore } from "@/store";
import MkVisibility from "@/components/MkVisibility.vue";
import MkVisibility from "@/components/MagVisibility.vue";
import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user";
@ -76,17 +78,20 @@ const showTicker =
border-radius: 100px;
font-size: 0.8em;
text-shadow: 0 2px 2px var(--shadow);
> .avatar {
width: 3.7em;
height: 3.7em;
margin-right: 1em;
}
> .user-info {
width: 0;
flex-grow: 1;
line-height: 1.5;
display: flex;
font-size: 1.2em;
> div {
&:first-child {
flex-grow: 1;
@ -95,22 +100,27 @@ const showTicker =
text-overflow: ellipsis;
gap: 0.1em 0;
}
&:last-child {
max-width: 50%;
gap: 0.3em 0.5em;
}
.article > .main & {
display: flex;
flex-direction: column;
align-items: flex-start;
&:last-child {
align-items: flex-end;
}
> * {
max-width: 100%;
}
}
}
.name {
// flex: 1 1 0px;
display: inline;
@ -150,6 +160,7 @@ const showTicker =
flex-shrink: 0;
margin-left: 0.5em;
font-size: 0.9em;
.created-at {
max-width: 100%;
overflow: hidden;
@ -161,6 +172,7 @@ const showTicker =
display: inline-flex;
margin-left: 0.5em;
vertical-align: middle;
> .name {
display: none;
}

View File

@ -3,7 +3,7 @@
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from "vue";
import { reactive, watch } from "vue";
import gsap from "gsap";
import number from "@/filters/number";
@ -22,6 +22,6 @@ watch(
},
{
immediate: true,
},
}
);
</script>

View File

@ -101,8 +101,8 @@
<XNoteSimple v-if="renote" class="preview" :note="renote" />
<div v-if="quoteId" class="with-quote">
<i class="ph-quotes ph-bold ph-lg"></i>
{{ i18n.ts.quoteAttached
}}<button class="_button" @click="quoteId = null">
{{ i18n.ts.quoteAttached }}
<button class="_button" @click="quoteId = null">
<i class="ph-x ph-bold ph-lg"></i>
</button>
</div>
@ -127,8 +127,8 @@
>{{ i18n.ts.notSpecifiedMentionWarning }} -
<button class="_textButton" @click="addMissingMention()">
{{ i18n.ts.add }}
</button></MkInfo
>
</button>
</MkInfo>
<input
v-show="useCw"
ref="cwInputEl"
@ -236,13 +236,14 @@
<script lang="ts" setup>
import {
computed,
defineAsyncComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
watch,
ref,
computed,
watch,
} from "vue";
import * as mfm from "mfm-js";
import * as misskey from "calckey-js";
@ -285,7 +286,7 @@ const props = withDefaults(
mention?: misskey.entities.User;
specified?: misskey.entities.User;
initialText?: string;
initialVisibility?: typeof misskey.noteVisibilities;
initialVisibility?: (typeof misskey.noteVisibilities)[number];
initialFiles?: (
| packed.PackDriveFileBase
| misskey.entities.DriveFile
@ -303,7 +304,7 @@ const props = withDefaults(
initialVisibleUsers: () => [],
autofocus: true,
showMfmCheatSheet: true,
},
}
);
const emit = defineEmits<{
@ -332,20 +333,20 @@ let cw = ref<string | null>(null);
let localOnly = ref<boolean>(
props.initialLocalOnly ?? defaultStore.state.rememberNoteVisibility
? defaultStore.state.localOnly
: defaultStore.state.defaultNoteLocalOnly,
: defaultStore.state.defaultNoteLocalOnly
);
let visibility = ref(
props.initialVisibility ??
((defaultStore.state.rememberNoteVisibility
? defaultStore.state.visibility
: defaultStore.state
.defaultNoteVisibility) as (typeof misskey.noteVisibilities)[number]),
.defaultNoteVisibility) as (typeof misskey.noteVisibilities)[number])
);
let visibleUsers = ref([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(pushVisibleUser);
}
let autocomplete = ref(null);
let autocomplete = ref<Autocomplete[] | null>(null);
let draghover = ref(false);
let quoteId = ref<string | null>(null);
let hasNotSpecifiedMentions = ref(false);
@ -419,9 +420,11 @@ const canPost = computed((): boolean => {
});
const withHashtags = computed(
defaultStore.makeGetterSetter("postFormWithHashtags"),
defaultStore.makeGetterSetter("postFormWithHashtags")
);
const hashtags = computed<string>(
defaultStore.makeGetterSetter("postFormHashtags")
);
const hashtags = computed(defaultStore.makeGetterSetter("postFormHashtags"));
watch(text, () => {
checkMissingMention();
@ -434,7 +437,7 @@ watch(
},
{
deep: true,
},
}
);
if (props.mention) {
@ -482,7 +485,7 @@ if (props.reply && props.reply.text != null) {
if (
props.reply &&
["home", "followers", "specified"].includes(
magLegacyVisibility(props.reply.visibility),
magLegacyVisibility(props.reply.visibility)
)
) {
if (
@ -492,7 +495,7 @@ if (
visibility.value = "followers";
} else if (
["home", "followers"].includes(
magLegacyVisibility(props.reply.visibility),
magLegacyVisibility(props.reply.visibility)
) &&
visibility.value === "specified"
) {
@ -505,7 +508,7 @@ if (
if (ids) {
os.api("users/show", {
userIds: ids.filter(
(uid) => uid !== $i.id && uid !== props.reply!.user.id,
(uid) => uid !== $i.id && uid !== props.reply!.user.id
),
}).then((users) => {
users.forEach(pushVisibleUser);
@ -516,7 +519,7 @@ if (
os.api("users/show", { userId: props.reply.user.id }).then(
(user) => {
pushVisibleUser(user);
},
}
);
}
}
@ -550,7 +553,7 @@ function checkMissingMention() {
for (const x of extractMentions(ast)) {
if (
!visibleUsers.value.some(
(u) => u.username === x.username && u.host === x.host,
(u) => u.username === x.username && u.host === x.host
)
) {
hasNotSpecifiedMentions.value = true;
@ -567,13 +570,13 @@ function addMissingMention() {
for (const x of extractMentions(ast)) {
if (
!visibleUsers.value.some(
(u) => u.username === x.username && u.host === x.host,
(u) => u.username === x.username && u.host === x.host
)
) {
os.api("users/show", { username: x.username, host: x.host }).then(
(user) => {
visibleUsers.value.push(user);
},
}
);
}
}
@ -601,7 +604,7 @@ function focus() {
textareaEl.value.focus();
textareaEl.value.setSelectionRange(
textareaEl.value.value.length,
textareaEl.value.value.length,
textareaEl.value.value.length
);
}
}
@ -612,7 +615,7 @@ function chooseFileFrom(ev) {
for (const file of files_) {
files.value.push(file);
}
},
}
);
}
@ -644,7 +647,7 @@ function upload(file: File, name?: string) {
function setVisibility() {
os.popup(
defineAsyncComponent(
() => import("@/components/MkVisibilityPicker.vue"),
() => import("@/components/MkVisibilityPicker.vue")
),
{
currentVisibility: visibility.value,
@ -665,14 +668,14 @@ function setVisibility() {
}
},
},
"closed",
"closed"
);
}
function pushVisibleUser(user) {
if (
!visibleUsers.value.some(
(u) => u.username === user.username && u.host === user.host,
(u) => u.username === user.username && u.host === user.host
)
) {
visibleUsers.value.push(user);
@ -697,13 +700,9 @@ function clear() {
}
function onKeydown(ev: KeyboardEvent) {
if (
(ev.which === 10 || ev.which === 13) &&
(ev.ctrlKey || ev.metaKey) &&
canPost.value
)
if (ev.key === "Enter" && (ev.ctrlKey || ev.metaKey) && canPost.value)
post();
if (ev.which === 27) emit("esc");
if (ev.key === "Escape") emit("esc");
}
function onCompositionUpdate(ev: CompositionEvent) {
@ -715,16 +714,24 @@ function onCompositionEnd(ev: CompositionEvent) {
}
async function onPaste(ev: ClipboardEvent) {
if (!ev.clipboardData) {
return;
}
for (const { item, i } of Array.from(ev.clipboardData.items).map(
(item, i) => ({ item, i }),
(item, i) => ({ item, i })
)) {
if (item.kind === "file") {
const file = item.getAsFile();
if (!file) {
continue;
}
const lio = file.name.lastIndexOf(".");
const ext = lio >= 0 ? file.name.slice(lio) : "";
const formatted = `${formatTimeString(
new Date(file.lastModified),
defaultStore.state.pastedFileName,
defaultStore.state.pastedFileName
).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
upload(file, formatted);
}
@ -744,9 +751,9 @@ async function onPaste(ev: ClipboardEvent) {
return;
}
quoteId.value = paste
.substring(url.length)
.match(/^\/notes\/(.+?)\/?$/)[1];
quoteId.value =
paste.substring(url.length).match(/^\/notes\/(.+?)\/?$/)?.[1] ??
null;
});
}
}
@ -872,7 +879,7 @@ async function post() {
if (postAccount.value) {
const storedAccounts = await getAccounts();
token = storedAccounts.find(
(x) => x.id === postAccount.value.id,
(x) => x.id === postAccount.value.id
)?.token;
}
@ -889,11 +896,11 @@ async function post() {
.filter((x) => x.type === "hashtag")
.map((x) => x.props.hashtag);
const history = JSON.parse(
localStorage.getItem("hashtags") || "[]",
localStorage.getItem("hashtags") || "[]"
) as string[];
localStorage.setItem(
"hashtags",
JSON.stringify(unique(hashtags_.concat(history))),
JSON.stringify(unique(hashtags_.concat(history)))
);
}
posting.value = false;
@ -943,7 +950,7 @@ function openAccountMenu(ev: MouseEvent) {
}
},
},
ev,
ev
);
}
@ -957,9 +964,11 @@ onMounted(() => {
}
// TODO: detach when unmount
new Autocomplete(textareaEl.value, text);
new Autocomplete(cwInputEl.value, cw);
new Autocomplete(hashtagsInputEl.value, hashtags);
autocomplete.value = [
new Autocomplete(textareaEl.value!, text),
new Autocomplete(cwInputEl.value!, cw),
new Autocomplete(hashtagsInputEl.value!, hashtags),
];
nextTick(() => {
// 稿
@ -974,7 +983,7 @@ onMounted(() => {
visibility.value = draft.data.visibility;
localOnly.value = draft.data.localOnly;
files.value = (draft.data.files || []).filter(
(draftFile) => draftFile,
(draftFile) => draftFile
);
if (draft.data.poll) {
poll.value = draft.data.poll;
@ -1007,6 +1016,10 @@ onMounted(() => {
nextTick(() => watchForDraft());
});
});
onBeforeUnmount(() => {
autocomplete.value?.forEach((a) => a.detach());
});
</script>
<style lang="scss" scoped>

View File

@ -64,15 +64,14 @@ const prependMany = async () => {
{ context: true, attachments: true },
{
id: note,
},
),
),
}
)
)
)
)
.filter((p) => p.status === "fulfilled")
.map(
(p) =>
(p as PromiseFulfilledResult<packed.PackNoteMaybeFull>).value,
(p) => (p as PromiseFulfilledResult<packed.PackNoteMaybeFull>).value
);
for (const n of notes) {
@ -247,11 +246,4 @@ onUnmounted(() => {
connection.dispose();
if (connection2) connection2.dispose();
});
/* TODO
const timetravel = (date?: Date) => {
this.date = date;
this.$refs.tl.reload();
};
*/
</script>

View File

@ -27,12 +27,12 @@
:target="target"
@click.stop
>
<img class="inner" :src="url" decoding="async" />
<img width="32" height="32" class="inner" :src="url" decoding="async" />
</MkA>
</template>
<script lang="ts" setup>
import { watch, computed, ref } from "vue";
import { computed, ref, watch } from "vue";
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import { extractAvgColorFromBlurhash } from "@/scripts/extract-avg-color-from-blurhash";
import { acct, userPage } from "@/filters/user";
@ -50,7 +50,7 @@ const props = withDefaults(
target: null,
disableLink: false,
disablePreview: false,
},
}
);
const emit = defineEmits<{
@ -60,7 +60,7 @@ const emit = defineEmits<{
const url = computed(() =>
defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.user.avatar_url)
: props.user.avatar_url,
: props.user.avatar_url
);
function onClick(ev: MouseEvent) {
@ -76,7 +76,7 @@ watch(
},
{
immediate: true,
},
}
);
</script>

View File

@ -8,7 +8,7 @@
import { h, shallowRef, VNodeArrayChildren, VNodeChild, watch } from "vue";
import * as mfm from "mfm-js";
import MkUrl from "@/components/global/MkUrl.vue";
import MkLink from "@/components/MkLink.vue";
import MkLink from "@/components/MagLink.vue";
import MagMention from "@/components/MagMention.vue";
import MagMatrixMention from "@/components/MagMatrixMention.vue";
import { concat } from "@/scripts/array";
@ -50,7 +50,7 @@ const props = withDefaults(
nowrap: false,
author: null,
isNote: true,
},
}
);
function render() {
@ -59,7 +59,7 @@ function render() {
let result: Result<MagNode[], MagnetarParseError> | null = null;
if (props.mm) {
result = parseMagnetarMarkdownXml(props.mm).flatMap(
magnetarMarkdownToMfm,
magnetarMarkdownToMfm
);
}
@ -98,7 +98,7 @@ function render() {
case "text": {
const text = token.props.text.replace(
/(\r\n|\n|\r)/g,
"\n",
"\n"
);
if (!props.plain) {
@ -129,7 +129,7 @@ function render() {
{
style: "font-style: oblique;",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -277,7 +277,7 @@ function render() {
{
class: "mfm-x2",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -288,7 +288,7 @@ function render() {
{
class: "mfm-x3",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -299,7 +299,7 @@ function render() {
{
class: "mfm-x4",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -327,7 +327,7 @@ function render() {
{
class: "_blur_text",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -344,26 +344,26 @@ function render() {
}
case "position": {
const x = parseFloat(
"" + token.props.args.x ?? "0",
"" + token.props.args.x ?? "0"
);
const y = parseFloat(
"" + token.props.args.y ?? "0",
"" + token.props.args.y ?? "0"
);
style = `transform: translateX(${x}em) translateY(${y}em);`;
break;
}
case "crop": {
const top = parseFloat(
"" + token.props.args.top ?? "0",
"" + token.props.args.top ?? "0"
);
const right = parseFloat(
"" + token.props.args.right ?? "0",
"" + token.props.args.right ?? "0"
);
const bottom = parseFloat(
"" + token.props.args.bottom ?? "0",
"" + token.props.args.bottom ?? "0"
);
const left = parseFloat(
"" + token.props.args.left ?? "0",
"" + token.props.args.left ?? "0"
);
style = `clip-path: inset(${top}% ${right}% ${bottom}% ${left}%);`;
break;
@ -371,11 +371,11 @@ function render() {
case "scale": {
const x = Math.min(
parseFloat("" + token.props.args.x ?? "1"),
5,
5
);
const y = Math.min(
parseFloat("" + token.props.args.y ?? "1"),
5,
5
);
style = `transform: scale(${x}, ${y});`;
break;
@ -401,7 +401,7 @@ function render() {
{
style: "opacity: 0.7;",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -412,7 +412,7 @@ function render() {
{
style: "text-align: center;",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -424,7 +424,7 @@ function render() {
{
style: `display: inline-block;${style}`,
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -447,7 +447,7 @@ function render() {
{
style: "opacity: 0.7;",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -459,7 +459,7 @@ function render() {
{
style: "text-align: center;",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -483,7 +483,7 @@ function render() {
url: token.props.url,
rel: "nofollow noopener",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -520,11 +520,11 @@ function render() {
{
key: Math.random(),
to: `/tags/${encodeURIComponent(
token.props.hashtag,
token.props.hashtag
)}`,
style: "color:var(--hashtag);",
},
`#${token.props.hashtag}`,
`#${token.props.hashtag}`
),
];
}
@ -559,7 +559,7 @@ function render() {
{
class: "quote",
},
genEl(token.children),
genEl(token.children)
),
];
}
@ -576,12 +576,12 @@ function render() {
`:${magTransProperty(
e,
"shortcode",
"name",
)}:`,
"name"
)}:`
),
{ name, host, url: null! },
),
)?.url ?? null!,
{ name, host, url: null! }
)
)?.url ?? null!
);
if (magIsMissingEmoji(emoji)) {
@ -630,7 +630,7 @@ function render() {
case "search": {
const sentinel = "#";
let ast2 = (props.plain ? mfm.parseSimple : mfm.parse)(
token.props.content + sentinel,
token.props.content + sentinel
);
const lastNode = ast2[ast2.length - 1];
@ -644,7 +644,7 @@ function render() {
) {
lastNode.props.text = lastNode.props.text.slice(
0,
-1,
-1
);
}
@ -672,13 +672,13 @@ function render() {
default: {
console.error(
"unrecognized ast type:",
(token as any)?.type,
(token as any)?.type
);
return [];
}
}
}),
})
);
return h("span", genEl(ast));
@ -692,6 +692,6 @@ watch(
},
{
immediate: true,
},
}
);
</script>

View File

@ -27,7 +27,7 @@ import { ComponentProps } from "vue-component-type-helpers";
export const pendingApiRequestsCount = ref(0);
const apiClient = new Misskey.api.APIClient({
origin: url,
origin: url
});
const magnetarApiClient = new MagApiClient(`${url}/mag/v1`);
@ -49,7 +49,7 @@ export async function magApi<
| string
| number;
},
token?: string | null | undefined,
token?: string | null | undefined
): Promise<
T["paginated"] extends true ? PaginatedResult<T["response"]> : T["response"]
> {
@ -60,7 +60,7 @@ export async function magApi<
endpoint,
data,
pathParams,
token || $i?.token,
token || $i?.token
);
} finally {
pendingApiRequestsCount.value--;
@ -75,7 +75,7 @@ export async function feApi<T extends keyof FrontendApiEndpoints & string>(
FrontendApiEndpoints[T]["response"]
>,
data: FrontendApiEndpoints[T]["request"],
token?: string | null | undefined,
token?: string | null | undefined
): Promise<FrontendApiEndpoints[T]["response"]> {
type Response = FrontendApiEndpoints[T]["response"];
@ -104,7 +104,7 @@ export async function feApi<T extends keyof FrontendApiEndpoints & string>(
endpointDef.method !== "GET" ? JSON.stringify(data) : undefined,
credentials: "omit",
cache: "no-cache",
headers: authorization ? { authorization } : {},
headers: authorization ? { authorization } : {}
})
.then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@ -130,7 +130,7 @@ export async function feApi<T extends keyof FrontendApiEndpoints & string>(
export const api = ((
endpoint: string,
data: Record<string, any> = {},
token?: string | null | undefined,
token?: string | null | undefined
) => {
pendingApiRequestsCount.value++;
@ -151,8 +151,8 @@ export const api = ((
body: JSON.stringify(data),
credentials: "omit",
cache: "no-cache",
headers: authorization ? { authorization } : {},
},
headers: authorization ? { authorization } : {}
}
)
.then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@ -176,7 +176,7 @@ export const api = ((
export const apiGet = ((
endpoint: string,
data: Record<string, any> = {},
token?: string | null | undefined,
token?: string | null | undefined
) => {
pendingApiRequestsCount.value++;
@ -197,7 +197,7 @@ export const apiGet = ((
method: "GET",
credentials: "omit",
cache: "default",
headers: authorization ? { authorization } : {},
headers: authorization ? { authorization } : {}
})
.then(async (res) => {
const body = res.status === 204 ? null : await res.json();
@ -221,13 +221,13 @@ export const apiGet = ((
export const apiWithDialog = ((
endpoint: string,
data: Record<string, any> = {},
token?: string | null | undefined,
token?: string | null | undefined
) => {
const promise = api(endpoint, data, token);
promiseDialog(promise, null, (err) => {
alert({
type: "error",
text: err.message + "\n" + (err as any).id,
text: err.message + "\n" + (err as any).id
});
});
@ -238,7 +238,7 @@ export function promiseDialog<T extends Promise<any>>(
promise: T,
onSuccess?: ((res: any) => void) | null,
onFailure?: ((err: Error) => void) | null,
text?: string,
text?: string
): T {
const showing = ref(true);
const success = ref(false);
@ -262,7 +262,7 @@ export function promiseDialog<T extends Promise<any>>(
} else {
alert({
type: "error",
text: err,
text: err
});
}
});
@ -273,10 +273,10 @@ export function promiseDialog<T extends Promise<any>>(
{
success: success,
showing: showing,
text: text,
text: text
},
{},
"closed",
"closed"
);
return promise;
@ -294,16 +294,18 @@ export const popups = ref([]) as Ref<
const zIndexes = {
low: 1000000,
middle: 2000000,
high: 3000000,
high: 3000000
};
export function claimZIndex(
priority: "low" | "middle" | "high" = "low",
priority: "low" | "middle" | "high" = "low"
): number {
zIndexes[priority] += 100;
return zIndexes[priority];
}
let uniqueId = 0;
export function getUniqueId(): string {
return uniqueId++ + "";
}
@ -350,7 +352,7 @@ export async function popup<C extends Component>(
component: C,
props: ComponentPropsRef<C>,
events: Partial<ComponentEmit<C>> = {} as ComponentEmit<C>,
disposeEvent?: keyof ComponentEmit<C>,
disposeEvent?: keyof ComponentEmit<C>
) {
markRaw(component);
@ -367,16 +369,16 @@ export async function popup<C extends Component>(
events: disposeEvent
? {
...events,
[disposeEvent]: dispose,
[disposeEvent]: dispose
}
: events,
id,
id
};
popups.value.push(state);
return {
dispose,
dispose
};
}
@ -385,13 +387,13 @@ export function pageWindow(path: string) {
defineAsyncComponent({
loader: () => import("@/components/MkPageWindow.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
initialPath: path,
initialPath: path
},
{},
"closed",
"closed"
);
}
@ -400,13 +402,13 @@ export function modalPageWindow(path: string) {
defineAsyncComponent({
loader: () => import("@/components/MkModalPageWindow.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
initialPath: path,
initialPath: path
},
{},
"closed",
"closed"
);
}
@ -414,10 +416,10 @@ export function toast(message: string) {
popup(
MkToast,
{
message,
message
},
{},
"closed",
"closed"
);
}
@ -436,9 +438,9 @@ export function alert(props: {
{
done: (result) => {
resolve();
}
},
},
"closed",
"closed"
);
});
}
@ -455,14 +457,14 @@ export function confirm(props: {
MkDialog,
{
...props,
showCancelButton: true,
showCancelButton: true
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
}
},
},
"closed",
"closed"
);
});
}
@ -477,19 +479,19 @@ export function yesno(props: {
defineAsyncComponent({
loader: () => import("@/components/MkDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
...props,
showCancelButton: true,
isYesNo: true,
isYesNo: true
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
}
},
},
"closed",
"closed"
);
});
}
@ -508,7 +510,7 @@ export function inputText(props: {
| {
canceled: false;
result: string;
}
}
> {
return new Promise((resolve, reject) => {
popup(
@ -522,15 +524,15 @@ export function inputText(props: {
autocomplete: props.autocomplete,
default: props.default,
minLength: props.minLength,
maxLength: props.maxLength,
},
maxLength: props.maxLength
}
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
}
},
},
"closed",
"closed"
);
});
}
@ -545,14 +547,14 @@ export function inputParagraph(props: {
| {
canceled: false;
result: string;
}
}
> {
return new Promise((resolve, reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
title: props.title,
@ -560,15 +562,15 @@ export function inputParagraph(props: {
input: {
type: "paragraph",
placeholder: props.placeholder,
default: props.default,
},
default: props.default
}
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
}
},
},
"closed",
"closed"
);
});
}
@ -584,14 +586,14 @@ export function inputNumber(props: {
| {
canceled: false;
result: number;
}
}
> {
return new Promise((resolve, reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
title: props.title,
@ -600,15 +602,15 @@ export function inputNumber(props: {
type: "number",
placeholder: props.placeholder,
autocomplete: props.autocomplete,
default: props.default,
},
default: props.default
}
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
}
},
},
"closed",
"closed"
);
});
}
@ -623,7 +625,7 @@ export function inputDate(props: {
| {
canceled: false;
result: Date;
}
}
> {
return new Promise((resolve, reject) => {
popup(
@ -634,8 +636,8 @@ export function inputDate(props: {
input: {
type: "date",
placeholder: props.placeholder,
default: props.default,
},
default: props.default
}
},
{
done: (result) => {
@ -643,13 +645,13 @@ export function inputDate(props: {
result
? {
result: new Date(result.result),
canceled: false,
canceled: false
}
: { canceled: true },
: { canceled: true }
);
}
},
},
"closed",
"closed"
);
});
}
@ -675,13 +677,13 @@ export function select<C = any>(
}[];
}[];
}
),
)
): Promise<
| { canceled: true; result: undefined }
| {
canceled: false;
result: C;
}
}
> {
return new Promise((resolve, reject) => {
popup(
@ -692,15 +694,15 @@ export function select<C = any>(
select: {
items: props.items,
groupedItems: props.groupedItems,
default: props.default,
},
default: props.default
}
},
{
done: (result) => {
resolve(result ? result : { canceled: true });
}
},
},
"closed",
"closed"
);
});
}
@ -715,12 +717,12 @@ export function success(): Promise<void> {
MkWaitingDialog,
{
success: true,
showing: showing,
showing: showing
},
{
done: () => resolve(),
done: () => resolve()
},
"closed",
"closed"
);
});
}
@ -732,12 +734,12 @@ export function waiting(): Promise<void> {
MkWaitingDialog,
{
success: false,
showing: showing,
showing: showing
},
{
done: () => resolve(),
done: () => resolve()
},
"closed",
"closed"
);
});
}
@ -748,34 +750,34 @@ export function form(title, form) {
defineAsyncComponent({
loader: () => import("@/components/MkFormDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{ title, form },
{
done: (result) => {
resolve(result);
}
},
},
"closed",
"closed"
);
});
}
export async function selectUser() {
export async function selectUser(): Promise<Misskey.entities.User> {
return new Promise((resolve, reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkUserSelectDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{},
{
ok: (user) => {
resolve(user);
}
},
},
"closed",
"closed"
);
});
}
@ -786,15 +788,15 @@ export async function selectInstance(): Promise<Misskey.entities.Instance> {
defineAsyncComponent({
loader: () => import("@/components/MkInstanceSelectDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{},
{
ok: (instance) => {
resolve(instance);
}
},
},
"closed",
"closed"
);
});
}
@ -805,20 +807,20 @@ export async function selectDriveFile(multiple: boolean) {
defineAsyncComponent({
loader: () => import("@/components/MkDriveSelectDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
type: "file",
multiple,
multiple
},
{
done: (files) => {
if (files) {
resolve(multiple ? files : files[0]);
}
}
},
},
"closed",
"closed"
);
});
}
@ -829,20 +831,20 @@ export async function selectDriveFolder(multiple: boolean) {
defineAsyncComponent({
loader: () => import("@/components/MkDriveSelectDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
type: "folder",
multiple,
multiple
},
{
done: (folders) => {
if (folders) {
resolve(multiple ? folders : folders[0]);
}
}
},
},
"closed",
"closed"
);
});
}
@ -853,18 +855,18 @@ export async function pickEmoji(src: HTMLElement | null, opts) {
defineAsyncComponent({
loader: () => import("@/components/MkEmojiPickerDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
src,
...opts,
...opts
},
{
done: (emoji: types.Reaction) => {
resolve(magReactionToLegacy(emoji));
}
},
},
"closed",
"closed"
);
});
}
@ -873,25 +875,25 @@ export async function cropImage(
image: Misskey.entities.DriveFile,
options: {
aspectRatio: number;
},
}
): Promise<Misskey.entities.DriveFile> {
return new Promise((resolve, reject) => {
popup(
defineAsyncComponent({
loader: () => import("@/components/MkCropperDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
file: image,
aspectRatio: options.aspectRatio,
aspectRatio: options.aspectRatio
},
{
ok: (x) => {
resolve(x);
}
},
},
"closed",
"closed"
);
});
}
@ -904,10 +906,11 @@ type AwaitType<T> =
: T;
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
export async function openEmojiPicker(
src?: HTMLElement,
opts,
initialTextarea: typeof activeTextarea,
initialTextarea: typeof activeTextarea
) {
if (openingEmojiPicker) return;
@ -923,11 +926,11 @@ export async function openEmojiPicker(
const observer = new MutationObserver((records) => {
for (const record of records) {
for (const node of Array.from(record.addedNodes).filter(
(node) => node instanceof HTMLElement,
(node) => node instanceof HTMLElement
) as HTMLElement[]) {
const textareas = node.querySelectorAll("textarea, input");
for (const textarea of Array.from(textareas).filter(
(textarea) => textarea.dataset.preventEmojiInsert == null,
(textarea) => textarea.dataset.preventEmojiInsert == null
)) {
if (document.activeElement === textarea)
activeTextarea = textarea;
@ -943,18 +946,18 @@ export async function openEmojiPicker(
childList: true,
subtree: true,
attributes: false,
characterData: false,
characterData: false
});
openingEmojiPicker = await popup(
defineAsyncComponent({
loader: () => import("@/components/MkEmojiPickerDialog.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
src,
...opts,
...opts
},
{
chosen: (emoji: types.Reaction) => {
@ -967,8 +970,8 @@ export async function openEmojiPicker(
openingEmojiPicker!.dispose();
openingEmojiPicker = null;
observer.disconnect();
},
},
}
}
);
}
@ -980,7 +983,7 @@ export function popupMenu(
width?: number;
viaKeyboard?: boolean;
noReturnFocus?: boolean;
},
}
) {
return new Promise((resolve, reject) => {
let dispose;
@ -988,7 +991,7 @@ export function popupMenu(
defineAsyncComponent({
loader: () => import("@/components/MkPopupMenu.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
items,
@ -996,14 +999,14 @@ export function popupMenu(
width: options?.width,
align: options?.align,
viaKeyboard: options?.viaKeyboard,
noReturnFocus: options?.noReturnFocus,
noReturnFocus: options?.noReturnFocus
},
{
closed: () => {
resolve();
dispose();
},
},
}
}
).then((res) => {
dispose = res.dispose;
});
@ -1012,7 +1015,7 @@ export function popupMenu(
export function contextMenu(
items: MenuItem[] | Ref<MenuItem[]>,
ev: MouseEvent,
ev: MouseEvent
) {
ev.preventDefault();
return new Promise((resolve, reject) => {
@ -1021,18 +1024,18 @@ export function contextMenu(
defineAsyncComponent({
loader: () => import("@/components/MkContextMenu.vue"),
loadingComponent: MkWaitingDialog,
delay: 1000,
delay: 1000
}),
{
items,
ev,
ev
},
{
closed: () => {
resolve();
dispose();
},
},
}
}
).then((res) => {
dispose = res.dispose;
});
@ -1051,7 +1054,7 @@ export function post(props: Record<string, any> = {}) {
closed: () => {
resolve();
dispose();
},
}
}).then((res) => {
dispose = res.dispose;
});

View File

@ -137,10 +137,8 @@
<template #caption>
<MkLink
url="https://codeberg.org/calckey/calckey/activity"
>{{
i18n.ts._aboutMisskey.allContributors
}}</MkLink
>
>{{ i18n.ts._aboutMisskey.allContributors }}
</MkLink>
</template>
</FormSection>
</div>
@ -150,11 +148,11 @@
</template>
<script lang="ts" setup>
import { nextTick, onBeforeUnmount, ref, computed } from "vue";
import { computed, nextTick, onBeforeUnmount, ref } from "vue";
import { version } from "@/config";
import FormLink from "@/components/form/link.vue";
import FormSection from "@/components/form/section.vue";
import MkLink from "@/components/MkLink.vue";
import MkLink from "@/components/MagLink.vue";
import { physics } from "@/scripts/physics";
import { i18n } from "@/i18n";
import { defaultStore } from "@/store";

View File

@ -1,11 +1,12 @@
<template>
<MkStickyContainer>
<template #header
><MkPageHeader
<template #header>
<MkPageHeader
v-model:tab="tab"
:actions="headerActions"
:tabs="headerTabs"
/></template>
/>
</template>
<MkSpacer
v-if="instance"
:content-max="600"
@ -64,9 +65,9 @@
>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>{{
i18n.ts.administrator
}}</template>
<template #key
>{{ i18n.ts.administrator }}
</template>
<template #value
>{{
instance.maintainerName ||
@ -75,14 +76,14 @@
({{
instance.maintainerEmail ||
`(${i18n.ts.unknown})`
}})</template
>
}})
</template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.description }}</template>
<template #value>{{
instance.description
}}</template>
<template #value
>{{ instance.description }}
</template>
</MkKeyValue>
<FormSection v-if="iAmAdmin">
@ -92,84 +93,86 @@
v-model="suspended"
class="_formBlock"
@update:modelValue="toggleSuspend"
>{{
i18n.ts.stopActivityDelivery
}}</FormSwitch
>
>{{ i18n.ts.stopActivityDelivery }}
</FormSwitch>
<FormSwitch
v-model="isBlocked"
class="_formBlock"
@update:modelValue="toggleBlock"
>{{ i18n.ts.blockThisInstance }}</FormSwitch
>
>{{ i18n.ts.blockThisInstance }}
</FormSwitch>
<FormSwitch
v-model="isSilenced"
class="_formBlock"
@update:modelValue="toggleSilence"
>{{
i18n.ts.silenceThisInstance
}}</FormSwitch
>
>{{ i18n.ts.silenceThisInstance }}
</FormSwitch>
</FormSuspense>
<MkButton @click="refreshMetadata"
><i
class="ph-arrows-clockwise ph-bold ph-lg"
></i>
Refresh metadata</MkButton
>
Refresh metadata
</MkButton>
</FormSection>
<FormSection>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>{{
i18n.ts.registeredAt
}}</template>
<template #value
><MkTime
<template #key
>{{ i18n.ts.registeredAt }}
</template>
<template #value>
<MkTime
mode="detail"
:time="instance.caughtAt"
/></template>
/>
</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>{{
i18n.ts.updatedAt
}}</template>
<template #value
><MkTime
<template #key
>{{ i18n.ts.updatedAt }}
</template>
<template #value>
<MkTime
mode="detail"
:time="instance.infoUpdatedAt"
/></template>
/>
</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>{{
i18n.ts.latestRequestSentAt
}}</template>
<template #value
><MkTime
<template #key
>{{ i18n.ts.latestRequestSentAt }}
</template>
<template #value>
<MkTime
v-if="instance.latestRequestSentAt"
:time="instance.latestRequestSentAt"
/><span v-else>N/A</span></template
/>
<span v-else>N/A</span></template
>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>{{
i18n.ts.latestStatus
}}</template>
<template #value>{{
<template #key
>{{ i18n.ts.latestStatus }}
</template>
<template #value
>{{
instance.latestStatus
? instance.latestStatus
: "N/A"
}}</template>
}}
</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>{{
i18n.ts.latestRequestReceivedAt
}}</template>
<template #value
><MkTime
<template #key
>{{ i18n.ts.latestRequestReceivedAt }}
</template>
<template #value>
<MkTime
v-if="instance.latestRequestReceivedAt"
:time="instance.latestRequestReceivedAt"
/><span v-else>N/A</span></template
/>
<span v-else>N/A</span></template
>
</MkKeyValue>
</FormSection>
@ -177,15 +180,15 @@
<FormSection>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>Following (Pub)</template>
<template #value>{{
number(instance.followingCount)
}}</template>
<template #value
>{{ number(instance.followingCount) }}
</template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0">
<template #key>Followers (Sub)</template>
<template #value>{{
number(instance.followersCount)
}}</template>
<template #value
>{{ number(instance.followersCount) }}
</template>
</MkKeyValue>
</FormSection>
@ -195,32 +198,32 @@
:to="`https://${host}/.well-known/host-meta`"
external
style="margin-bottom: 8px"
>host-meta</FormLink
>
>host-meta
</FormLink>
<FormLink
:to="`https://${host}/.well-known/host-meta.json`"
external
style="margin-bottom: 8px"
>host-meta.json</FormLink
>
>host-meta.json
</FormLink>
<FormLink
:to="`https://${host}/.well-known/nodeinfo`"
external
style="margin-bottom: 8px"
>nodeinfo</FormLink
>
>nodeinfo
</FormLink>
<FormLink
:to="`https://${host}/robots.txt`"
external
style="margin-bottom: 8px"
>robots.txt</FormLink
>
>robots.txt
</FormLink>
<FormLink
:to="`https://${host}/manifest.json`"
external
style="margin-bottom: 8px"
>manifest.json</FormLink
>
>manifest.json
</FormLink>
</FormSection>
</div>
</swiper-slide>
@ -316,7 +319,7 @@
:key="user.id"
v-tooltip.mfm="
`Last posted: ${new Date(
user.updatedAt,
user.updatedAt
).toLocaleString()}`
"
class="user"
@ -329,7 +332,7 @@
</swiper-slide>
<swiper-slide>
<div class="_formRoot">
<MkObjectView tall :value="instance"> </MkObjectView>
<MkObjectView tall :value="instance"></MkObjectView>
</div>
</swiper-slide>
</swiper>
@ -338,14 +341,14 @@
</template>
<script lang="ts" setup>
import { watch, ref, computed } from "vue";
import { computed, ref, watch } from "vue";
import { Virtual } from "swiper";
import { Swiper, SwiperSlide } from "swiper/vue";
import type * as calckey from "calckey-js";
import MkChart from "@/components/MkChart.vue";
import MkObjectView from "@/components/MkObjectView.vue";
import FormLink from "@/components/form/link.vue";
import MkLink from "@/components/MkLink.vue";
import MkLink from "@/components/MagLink.vue";
import MkButton from "@/components/MkButton.vue";
import FormSection from "@/components/form/section.vue";
import MkKeyValue from "@/components/MkKeyValue.vue";
@ -425,7 +428,7 @@ async function toggleBlock() {
blockedHosts = meta.value.blockedHosts.concat([instance.value.host]);
} else {
blockedHosts = meta.value.blockedHosts.filter(
(x) => x !== instance.value!.host,
(x) => x !== instance.value!.host
);
}
await os.api("admin/update-meta", {
@ -443,7 +446,7 @@ async function toggleSilence() {
silencedHosts = meta.value.silencedHosts.concat([instance.value.host]);
} else {
silencedHosts = meta.value.silencedHosts.filter(
(x) => x !== instance.value!.host,
(x) => x !== instance.value!.host
);
}
await os.api("admin/update-meta", {
@ -503,7 +506,7 @@ if (iAmAdmin) {
key: "raw",
title: "Raw",
icon: "ph-code ph-bold ph-lg",
},
}
);
}

View File

@ -9,8 +9,8 @@
<I18n :src="i18n.ts.i18nInfo" tag="span">
<template #link>
<MkLink url="https://hosted.weblate.org/engage/calckey/"
>Weblate</MkLink
>
>Weblate
</MkLink>
</template>
</I18n>
</template>
@ -34,33 +34,33 @@
<FormSection>
<template #label>{{ i18n.ts.behavior }}</template>
<FormSwitch v-model="imageNewTab" class="_formBlock">{{
i18n.ts.openImageInNewTab
}}</FormSwitch>
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock">{{
i18n.ts.enableInfiniteScroll
}}</FormSwitch>
<FormSwitch v-model="imageNewTab" class="_formBlock"
>{{ i18n.ts.openImageInNewTab }}
</FormSwitch>
<FormSwitch v-model="enableInfiniteScroll" class="_formBlock"
>{{ i18n.ts.enableInfiniteScroll }}
</FormSwitch>
<FormSwitch
v-model="useReactionPickerForContextMenu"
class="_formBlock"
>{{ i18n.ts.useReactionPickerForContextMenu }}</FormSwitch
>
<FormSwitch v-model="swipeOnDesktop" class="_formBlock">{{
i18n.ts.swipeOnDesktop
}}</FormSwitch>
<FormSwitch v-model="enterSendsMessage" class="_formBlock">{{
i18n.ts.enterSendsMessage
}}</FormSwitch>
<FormSwitch v-model="disablePagesScript" class="_formBlock">{{
i18n.ts.disablePagesScript
}}</FormSwitch>
>{{ i18n.ts.useReactionPickerForContextMenu }}
</FormSwitch>
<FormSwitch v-model="swipeOnDesktop" class="_formBlock"
>{{ i18n.ts.swipeOnDesktop }}
</FormSwitch>
<FormSwitch v-model="enterSendsMessage" class="_formBlock"
>{{ i18n.ts.enterSendsMessage }}
</FormSwitch>
<FormSwitch v-model="disablePagesScript" class="_formBlock"
>{{ i18n.ts.disablePagesScript }}
</FormSwitch>
<FormSwitch v-model="showTimelineReplies" class="_formBlock"
>{{ i18n.ts.flagShowTimelineReplies
}}<template #caption
>{{ i18n.ts.flagShowTimelineReplies }}
<template #caption
>{{ i18n.ts.flagShowTimelineRepliesDescription }}
{{ i18n.ts.reflectMayTakeTime }}</template
></FormSwitch
>
{{ i18n.ts.reflectMayTakeTime }}
</template>
</FormSwitch>
<FormSelect v-model="serverDisconnectedBehavior" class="_formBlock">
<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@ -82,16 +82,16 @@
<FormSection>
<template #label>{{ i18n.ts.accessibility }}</template>
<FormSwitch v-model="expandOnNoteClick" class="_formBlock"
>{{ i18n.ts.expandOnNoteClick
}}<template #caption>{{
i18n.ts.expandOnNoteClickDesc
}}</template>
>{{ i18n.ts.expandOnNoteClick }}
<template #caption
>{{ i18n.ts.expandOnNoteClickDesc }}
</template>
</FormSwitch>
<FormSwitch v-model="advancedMfm" class="_formBlock">
{{ i18n.ts._mfm.advanced
}}<template #caption>{{
i18n.ts._mfm.advancedDescription
}}</template>
{{ i18n.ts._mfm.advanced }}
<template #caption
>{{ i18n.ts._mfm.advancedDescription }}
</template>
</FormSwitch>
<FormSwitch v-model="autoplayMfm" class="_formBlock">
{{ i18n.ts._mfm.alwaysPlay }}
@ -103,14 +103,14 @@
{{ i18n.ts._mfm.warn }}
</template>
</FormSwitch>
<FormSwitch v-model="reduceAnimation" class="_formBlock">{{
i18n.ts.reduceUiAnimation
}}</FormSwitch>
<FormSwitch v-model="reduceAnimation" class="_formBlock"
>{{ i18n.ts.reduceUiAnimation }}
</FormSwitch>
<FormSwitch
v-model="disableShowingAnimatedImages"
class="_formBlock"
>{{ i18n.ts.disableShowingAnimatedImages }}</FormSwitch
>
>{{ i18n.ts.disableShowingAnimatedImages }}
</FormSwitch>
<FormRadios v-model="fontSize" class="_formBlock">
<template #label>{{ i18n.ts.fontSize }}</template>
<option :value="null">
@ -146,44 +146,44 @@
<FormSection>
<template #label>{{ i18n.ts.appearance }}</template>
<FormSwitch v-model="useBlurEffect" class="_formBlock">{{
i18n.ts.useBlurEffect
}}</FormSwitch>
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock">{{
i18n.ts.useBlurEffectForModal
}}</FormSwitch>
<FormSwitch v-model="useBlurEffect" class="_formBlock"
>{{ i18n.ts.useBlurEffect }}
</FormSwitch>
<FormSwitch v-model="useBlurEffectForModal" class="_formBlock"
>{{ i18n.ts.useBlurEffectForModal }}
</FormSwitch>
<FormSwitch
v-model="showGapBetweenNotesInTimeline"
class="_formBlock"
>{{ i18n.ts.showGapBetweenNotesInTimeline }}</FormSwitch
>
<FormSwitch v-model="loadRawImages" class="_formBlock">{{
i18n.ts.loadRawImages
}}</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock">{{
i18n.ts.squareAvatars
}}</FormSwitch>
<FormSwitch v-model="seperateRenoteQuote" class="_formBlock">{{
i18n.ts.seperateRenoteQuote
}}</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock">{{
i18n.ts.useSystemFont
}}</FormSwitch>
>{{ i18n.ts.showGapBetweenNotesInTimeline }}
</FormSwitch>
<FormSwitch v-model="loadRawImages" class="_formBlock"
>{{ i18n.ts.loadRawImages }}
</FormSwitch>
<FormSwitch v-model="squareAvatars" class="_formBlock"
>{{ i18n.ts.squareAvatars }}
</FormSwitch>
<FormSwitch v-model="seperateRenoteQuote" class="_formBlock"
>{{ i18n.ts.seperateRenoteQuote }}
</FormSwitch>
<FormSwitch v-model="useSystemFont" class="_formBlock"
>{{ i18n.ts.useSystemFont }}
</FormSwitch>
<FormSwitch v-model="useOsNativeEmojis" class="_formBlock">
{{ i18n.ts.useOsNativeEmojis }}
<div>
<Mfm :key="useOsNativeEmojis" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪" />
</div>
</FormSwitch>
<FormSwitch v-model="disableDrawer" class="_formBlock">{{
i18n.ts.disableDrawer
}}</FormSwitch>
<FormSwitch v-model="showUpdates" class="_formBlock">{{
i18n.ts.showUpdates
}}</FormSwitch>
<FormSwitch v-model="showFixedPostForm" class="_formBlock">{{
i18n.ts.showFixedPostForm
}}</FormSwitch>
<FormSwitch v-model="disableDrawer" class="_formBlock"
>{{ i18n.ts.disableDrawer }}
</FormSwitch>
<FormSwitch v-model="showUpdates" class="_formBlock"
>{{ i18n.ts.showUpdates }}
</FormSwitch>
<FormSwitch v-model="showFixedPostForm" class="_formBlock"
>{{ i18n.ts.showFixedPostForm }}
</FormSwitch>
<FormSelect v-model="instanceTicker" class="_formBlock">
<template #label>{{ i18n.ts.instanceTicker }}</template>
<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
@ -212,19 +212,19 @@
class="_formBlock"
>
<template #label>{{ i18n.ts.numberOfPageCache }}</template>
<template #caption>{{
i18n.ts.numberOfPageCacheDescription
}}</template>
<template #caption
>{{ i18n.ts.numberOfPageCacheDescription }}
</template>
</FormRange>
<FormLink to="/settings/deck" class="_formBlock">{{
i18n.ts.deck
}}</FormLink>
<FormLink to="/settings/deck" class="_formBlock"
>{{ i18n.ts.deck }}
</FormLink>
<FormLink to="/settings/custom-katex-macro" class="_formBlock"
><template #icon><i class="ph-radical ph-bold ph-lg"></i></template
>{{ i18n.ts.customKaTeXMacro }}</FormLink
>
<FormLink to="/settings/custom-katex-macro" class="_formBlock">
<template #icon><i class="ph-radical ph-bold ph-lg"></i></template>
{{ i18n.ts.customKaTeXMacro }}
</FormLink>
</div>
</template>
@ -236,7 +236,7 @@ import FormRadios from "@/components/form/radios.vue";
import FormRange from "@/components/form/range.vue";
import FormSection from "@/components/form/section.vue";
import FormLink from "@/components/form/link.vue";
import MkLink from "@/components/MkLink.vue";
import MkLink from "@/components/MagLink.vue";
import { langs } from "@/config";
import { defaultStore } from "@/store";
import * as os from "@/os";
@ -259,24 +259,24 @@ async function reloadAsk() {
}
const overridedDeviceKind = computed(
defaultStore.makeGetterSetter("overridedDeviceKind"),
defaultStore.makeGetterSetter("overridedDeviceKind")
);
const serverDisconnectedBehavior = computed(
defaultStore.makeGetterSetter("serverDisconnectedBehavior"),
defaultStore.makeGetterSetter("serverDisconnectedBehavior")
);
const reduceAnimation = computed(
defaultStore.makeGetterSetter(
"animation",
(v) => !v,
(v) => !v,
),
(v) => !v
)
);
const useBlurEffectForModal = computed(
defaultStore.makeGetterSetter("useBlurEffectForModal"),
defaultStore.makeGetterSetter("useBlurEffectForModal")
);
const useBlurEffect = computed(defaultStore.makeGetterSetter("useBlurEffect"));
const showGapBetweenNotesInTimeline = computed(
defaultStore.makeGetterSetter("showGapBetweenNotesInTimeline"),
defaultStore.makeGetterSetter("showGapBetweenNotesInTimeline")
);
const showAds = computed(defaultStore.makeGetterSetter("showAds"));
const advancedMfm = computed(defaultStore.makeGetterSetter("advancedMfm"));
@ -284,53 +284,53 @@ const autoplayMfm = computed(
defaultStore.makeGetterSetter(
"animatedMfm",
(v) => !v,
(v) => !v,
),
(v) => !v
)
);
const useOsNativeEmojis = computed(
defaultStore.makeGetterSetter("useOsNativeEmojis"),
defaultStore.makeGetterSetter("useOsNativeEmojis")
);
const disableDrawer = computed(defaultStore.makeGetterSetter("disableDrawer"));
const disableShowingAnimatedImages = computed(
defaultStore.makeGetterSetter("disableShowingAnimatedImages"),
defaultStore.makeGetterSetter("disableShowingAnimatedImages")
);
const loadRawImages = computed(defaultStore.makeGetterSetter("loadRawImages"));
const imageNewTab = computed(defaultStore.makeGetterSetter("imageNewTab"));
const nsfw = computed(defaultStore.makeGetterSetter("nsfw"));
const disablePagesScript = computed(
defaultStore.makeGetterSetter("disablePagesScript"),
defaultStore.makeGetterSetter("disablePagesScript")
);
const expandOnNoteClick = computed(
defaultStore.makeGetterSetter("expandOnNoteClick"),
defaultStore.makeGetterSetter("expandOnNoteClick")
);
const showFixedPostForm = computed(
defaultStore.makeGetterSetter("showFixedPostForm"),
defaultStore.makeGetterSetter("showFixedPostForm")
);
const numberOfPageCache = computed(
defaultStore.makeGetterSetter("numberOfPageCache"),
defaultStore.makeGetterSetter("numberOfPageCache")
);
const instanceTicker = computed(
defaultStore.makeGetterSetter("instanceTicker"),
defaultStore.makeGetterSetter("instanceTicker")
);
const enableInfiniteScroll = computed(
defaultStore.makeGetterSetter("enableInfiniteScroll"),
defaultStore.makeGetterSetter("enableInfiniteScroll")
);
const enterSendsMessage = computed(
defaultStore.makeGetterSetter("enterSendsMessage"),
defaultStore.makeGetterSetter("enterSendsMessage")
);
const useReactionPickerForContextMenu = computed(
defaultStore.makeGetterSetter("useReactionPickerForContextMenu"),
defaultStore.makeGetterSetter("useReactionPickerForContextMenu")
);
const seperateRenoteQuote = computed(
defaultStore.makeGetterSetter("seperateRenoteQuote"),
defaultStore.makeGetterSetter("seperateRenoteQuote")
);
const squareAvatars = computed(defaultStore.makeGetterSetter("squareAvatars"));
const showUpdates = computed(defaultStore.makeGetterSetter("showUpdates"));
const swipeOnDesktop = computed(
defaultStore.makeGetterSetter("swipeOnDesktop"),
defaultStore.makeGetterSetter("swipeOnDesktop")
);
const showTimelineReplies = computed(
defaultStore.makeGetterSetter("showTimelineReplies"),
defaultStore.makeGetterSetter("showTimelineReplies")
);
watch(lang, () => {
@ -374,7 +374,7 @@ watch(
],
async () => {
await reloadAsk();
},
}
);
const headerActions = computed(() => []);

View File

@ -8,7 +8,7 @@
>
<div class="main">
<div class="profile">
<MkMoved
<MagMoved
v-if="user.moved_to"
:host="user.moved_to.host"
:acct="user.moved_to.username"
@ -274,10 +274,12 @@
<dd class="value">
{{
new Date(
user.created_at,
user.created_at
).toLocaleString()
}}
(<MkTime :time="user.created_at" />)
(
<MkTime :time="user.created_at" />
)
</dd>
</dl>
</div>
@ -337,8 +339,8 @@
<MkInfo
v-else-if="$i && $i.id === user.id"
style="margin: 12px 0"
>{{ i18n.ts.userPagePinTip }}</MkInfo
>
>{{ i18n.ts.userPagePinTip }}
</MkInfo>
<template v-if="narrow">
<XPhotos :key="user.id" :user="user" />
<XActivity
@ -366,11 +368,11 @@
<script lang="ts" setup>
import {
computed,
defineAsyncComponent,
onMounted,
onUnmounted,
ref,
computed,
} from "vue";
import calcAge from "s-age";
import cityTimezones from "city-timezones";
@ -378,7 +380,7 @@ import XUserTimeline from "./index.timeline.vue";
import MkFollowButton from "@/components/MkFollowButton.vue";
import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
import MkInfo from "@/components/MkInfo.vue";
import MkMoved from "@/components/MkMoved.vue";
import MagMoved from "@/components/MagMoved.vue";
import XNote from "@/components/MagNote.vue";
import { getScrollPosition } from "@/scripts/scroll";
import { userPage } from "@/filters/user";
@ -400,7 +402,7 @@ const props = withDefaults(
defineProps<{
user: packed.PackUserMaybeAll;
}>(),
{},
{}
);
let parallaxAnimationId = ref<null | number>(null);
@ -425,16 +427,16 @@ const timeForThem = computed(() => {
props.user
.location!.replace(
/[^A-Za-z0-9ÁĆÉǴÍḰĹḾŃÓṔŔŚÚÝŹáćéǵíḱĺḿńóṕŕśúýź\-'.s].*/,
"",
""
)
.trim(),
props.user.location!.replace(
/[^A-Za-zÁĆÉǴÍḰĹḾŃÓṔŔŚÚÝŹáćéǵíḱĺḿńóṕŕśúýź\-'.].*/,
"",
""
),
props.user.location!.replace(
/[^A-Za-zÁĆÉǴÍḰĹḾŃÓṔŔŚÚÝŹáćéǵíḱĺḿńóṕŕśúýź].*/,
"",
""
),
];
@ -668,6 +670,7 @@ onUnmounted(() => {
> .nameColumn {
display: block;
> .name {
margin: 0;
align-content: center;
@ -876,6 +879,7 @@ onUnmounted(() => {
height: auto;
border-bottom: 1px solid var(--divider);
padding-bottom: 5px;
> .actions {
position: static;
}

View File

@ -1,4 +1,4 @@
import { nextTick, Ref, ref, defineAsyncComponent } from "vue";
import { defineAsyncComponent, nextTick, ref, Ref } from "vue";
import getCaretCoordinates from "textarea-caret";
import { toASCII } from "punycode/";
import { popup } from "@/os";
@ -28,7 +28,7 @@ export class Autocomplete {
*/
constructor(
textarea: HTMLInputElement | HTMLTextAreaElement,
textRef: Ref<string>,
textRef: Ref<string>
) {
//#region BIND
this.onInput = this.onInput.bind(this);
@ -63,8 +63,9 @@ export class Autocomplete {
*
*/
private onInput() {
const caretPos = this.textarea.selectionStart;
const text = this.text.substr(0, caretPos).split("\n").pop()!;
const caretPos = this.textarea.selectionStart ?? undefined;
const text = this.text.substring(0, caretPos).split("\n").pop()!;
const mentionIndex = text.lastIndexOf("@");
const hashtagIndex = text.lastIndexOf("#");
@ -75,7 +76,7 @@ export class Autocomplete {
mentionIndex,
hashtagIndex,
emojiIndex,
mfmTagIndex,
mfmTagIndex
);
if (max === -1) {
@ -96,7 +97,7 @@ export class Autocomplete {
let opened = false;
if (isMention) {
const username = text.substr(mentionIndex + 1);
const username = text.substring(mentionIndex + 1);
if (username !== "" && username.match(/^[a-zA-Z0-9_]+$/)) {
this.open("user", username);
opened = true;
@ -107,7 +108,7 @@ export class Autocomplete {
}
if (isHashtag && !opened) {
const hashtag = text.substr(hashtagIndex + 1);
const hashtag = text.substring(hashtagIndex + 1);
if (!hashtag.includes(" ")) {
this.open("hashtag", hashtag);
opened = true;
@ -115,7 +116,7 @@ export class Autocomplete {
}
if (isEmoji && !opened) {
const emoji = text.substr(emojiIndex + 1);
const emoji = text.substring(emojiIndex + 1);
if (!emoji.includes(" ")) {
this.open("emoji", emoji);
opened = true;
@ -123,7 +124,7 @@ export class Autocomplete {
}
if (isMfmTag && !opened) {
const mfmTag = text.substr(mfmTagIndex + 1);
const mfmTag = text.substring(mfmTagIndex + 1);
if (!mfmTag.includes(" ")) {
this.open("mfmTag", mfmTag.replace("[", ""));
opened = true;
@ -149,7 +150,7 @@ export class Autocomplete {
//#region サジェストを表示すべき位置を計算
const caretPosition = getCaretCoordinates(
this.textarea,
this.textarea.selectionStart,
this.textarea.selectionStart
);
const rect = this.textarea.getBoundingClientRect();
@ -171,7 +172,7 @@ export class Autocomplete {
const { dispose } = await popup(
defineAsyncComponent(
() => import("@/components/MkAutocomplete.vue"),
() => import("@/components/MkAutocomplete.vue")
),
{
textarea: this.textarea,
@ -185,7 +186,7 @@ export class Autocomplete {
done: (res) => {
this.complete(res);
},
},
}
);
this.suggestion = {
@ -217,14 +218,14 @@ export class Autocomplete {
private complete({ type, value }) {
this.close();
const caret = this.textarea.selectionStart;
const caret = this.textarea.selectionStart ?? undefined;
if (type === "user") {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf("@"));
const after = source.substr(caret);
const after = source.substring(caret);
const acct =
value.host === null
@ -243,9 +244,9 @@ export class Autocomplete {
} else if (type === "hashtag") {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf("#"));
const after = source.substr(caret);
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}#${value} ${after}`;
@ -259,9 +260,9 @@ export class Autocomplete {
} else if (type === "emoji") {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf(":"));
const after = source.substr(caret);
const after = source.substring(caret);
// 挿入
this.text = trimmedBefore + value + after;
@ -275,9 +276,9 @@ export class Autocomplete {
} else if (type === "mfmTag") {
const source = this.text;
const before = source.substr(0, caret);
const before = source.substring(0, caret);
const trimmedBefore = before.substring(0, before.lastIndexOf("$"));
const after = source.substr(caret);
const after = source.substring(caret);
// 挿入
this.text = `${trimmedBefore}$[${value} ]${after}`;

View File

@ -118,13 +118,13 @@ importers:
version: 2.1.1
'@rollup/plugin-alias':
specifier: ^5.1.0
version: 5.1.0(rollup@4.17.1)
version: 5.1.0(rollup@4.17.2)
'@rollup/plugin-json':
specifier: ^6.1.0
version: 6.1.0(rollup@4.17.1)
version: 6.1.0(rollup@4.17.2)
'@rollup/pluginutils':
specifier: ^5.1.0
version: 5.1.0(rollup@4.17.1)
version: 5.1.0(rollup@4.17.2)
'@types/escape-regexp':
specifier: 0.0.1
version: 0.0.1
@ -273,8 +273,8 @@ importers:
specifier: 1.0.0
version: 1.0.0
rollup:
specifier: ^4.17.1
version: 4.17.1
specifier: ^4.17.2
version: 4.17.2
s-age:
specifier: 1.1.2
version: 1.1.2
@ -345,8 +345,8 @@ importers:
specifier: ^3.4.26
version: 3.4.26(typescript@5.4.5)
vue-component-type-helpers:
specifier: ^2.0.14
version: 2.0.14
specifier: ^2.0.15
version: 2.0.15
vue-isyourpasswordsafe:
specifier: ^2.0.0
version: 2.0.0
@ -426,13 +426,13 @@ packages:
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier@7.22.20:
resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
/@babel/helper-validator-identifier@7.22.5:
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
engines: {node: '>=6.9.0'}
dev: true
/@babel/helper-validator-identifier@7.22.5:
resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
/@babel/helper-validator-identifier@7.24.5:
resolution: {integrity: sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==}
engines: {node: '>=6.9.0'}
dev: true
@ -445,12 +445,12 @@ packages:
js-tokens: 4.0.0
dev: true
/@babel/parser@7.24.4:
resolution: {integrity: sha512-zTvEBcghmeBma9QIGunWevvBAp4/Qu9Bdq+2k0Ot4fVMD6v3dsC9WOcRSKk7tRRyBM/53yKMJko9xOatGQAwSg==}
/@babel/parser@7.24.5:
resolution: {integrity: sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==}
engines: {node: '>=6.0.0'}
hasBin: true
dependencies:
'@babel/types': 7.24.0
'@babel/types': 7.24.5
dev: true
/@babel/runtime@7.20.7:
@ -460,19 +460,19 @@ packages:
regenerator-runtime: 0.13.11
dev: true
/@babel/runtime@7.24.4:
resolution: {integrity: sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==}
/@babel/runtime@7.24.5:
resolution: {integrity: sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.1
dev: true
/@babel/types@7.24.0:
resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==}
/@babel/types@7.24.5:
resolution: {integrity: sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==}
engines: {node: '>=6.9.0'}
dependencies:
'@babel/helper-string-parser': 7.24.1
'@babel/helper-validator-identifier': 7.22.20
'@babel/helper-validator-identifier': 7.24.5
to-fast-properties: 2.0.0
dev: true
@ -979,7 +979,7 @@ packages:
resolution: {integrity: sha512-QjrfbItu5Rb2i37GzsKxmrRHfZPTVk3oXSPBnQ2+oACDbQRWGAeB0AsvZw263n1nFouQuff+khOCtRbrc6+k+A==}
dev: true
/@rollup/plugin-alias@5.1.0(rollup@4.17.1):
/@rollup/plugin-alias@5.1.0(rollup@4.17.2):
resolution: {integrity: sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
@ -988,11 +988,11 @@ packages:
rollup:
optional: true
dependencies:
rollup: 4.17.1
rollup: 4.17.2
slash: 4.0.0
dev: true
/@rollup/plugin-json@6.1.0(rollup@4.17.1):
/@rollup/plugin-json@6.1.0(rollup@4.17.2):
resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==}
engines: {node: '>=14.0.0'}
peerDependencies:
@ -1001,11 +1001,11 @@ packages:
rollup:
optional: true
dependencies:
'@rollup/pluginutils': 5.1.0(rollup@4.17.1)
rollup: 4.17.1
'@rollup/pluginutils': 5.1.0(rollup@4.17.2)
rollup: 4.17.2
dev: true
/@rollup/pluginutils@5.1.0(rollup@4.17.1):
/@rollup/pluginutils@5.1.0(rollup@4.17.2):
resolution: {integrity: sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==}
engines: {node: '>=14.0.0'}
peerDependencies:
@ -1017,131 +1017,131 @@ packages:
'@types/estree': 1.0.5
estree-walker: 2.0.2
picomatch: 2.3.1
rollup: 4.17.1
rollup: 4.17.2
dev: true
/@rollup/rollup-android-arm-eabi@4.17.1:
resolution: {integrity: sha512-P6Wg856Ou/DLpR+O0ZLneNmrv7QpqBg+hK4wE05ijbC/t349BRfMfx+UFj5Ha3fCFopIa6iSZlpdaB4agkWp2Q==}
/@rollup/rollup-android-arm-eabi@4.17.2:
resolution: {integrity: sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==}
cpu: [arm]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-android-arm64@4.17.1:
resolution: {integrity: sha512-piwZDjuW2WiHr05djVdUkrG5JbjnGbtx8BXQchYCMfib/nhjzWoiScelZ+s5IJI7lecrwSxHCzW026MWBL+oJQ==}
/@rollup/rollup-android-arm64@4.17.2:
resolution: {integrity: sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==}
cpu: [arm64]
os: [android]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-arm64@4.17.1:
resolution: {integrity: sha512-LsZXXIsN5Q460cKDT4Y+bzoPDhBmO5DTr7wP80d+2EnYlxSgkwdPfE3hbE+Fk8dtya+8092N9srjBTJ0di8RIA==}
/@rollup/rollup-darwin-arm64@4.17.2:
resolution: {integrity: sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==}
cpu: [arm64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-darwin-x64@4.17.1:
resolution: {integrity: sha512-S7TYNQpWXB9APkxu/SLmYHezWwCoZRA9QLgrDeml+SR2A1LLPD2DBUdUlvmCF7FUpRMKvbeeWky+iizQj65Etw==}
/@rollup/rollup-darwin-x64@4.17.2:
resolution: {integrity: sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==}
cpu: [x64]
os: [darwin]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm-gnueabihf@4.17.1:
resolution: {integrity: sha512-Lq2JR5a5jsA5um2ZoLiXXEaOagnVyCpCW7xvlcqHC7y46tLwTEgUSTM3a2TfmmTMmdqv+jknUioWXlmxYxE9Yw==}
/@rollup/rollup-linux-arm-gnueabihf@4.17.2:
resolution: {integrity: sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm-musleabihf@4.17.1:
resolution: {integrity: sha512-9BfzwyPNV0IizQoR+5HTNBGkh1KXE8BqU0DBkqMngmyFW7BfuIZyMjQ0s6igJEiPSBvT3ZcnIFohZ19OqjhDPg==}
/@rollup/rollup-linux-arm-musleabihf@4.17.2:
resolution: {integrity: sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==}
cpu: [arm]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-gnu@4.17.1:
resolution: {integrity: sha512-e2uWaoxo/rtzA52OifrTSXTvJhAXb0XeRkz4CdHBK2KtxrFmuU/uNd544Ogkpu938BzEfvmWs8NZ8Axhw33FDw==}
/@rollup/rollup-linux-arm64-gnu@4.17.2:
resolution: {integrity: sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-arm64-musl@4.17.1:
resolution: {integrity: sha512-ekggix/Bc/d/60H1Mi4YeYb/7dbal1kEDZ6sIFVAE8pUSx7PiWeEh+NWbL7bGu0X68BBIkgF3ibRJe1oFTksQQ==}
/@rollup/rollup-linux-arm64-musl@4.17.2:
resolution: {integrity: sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==}
cpu: [arm64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-powerpc64le-gnu@4.17.1:
resolution: {integrity: sha512-UGV0dUo/xCv4pkr/C8KY7XLFwBNnvladt8q+VmdKrw/3RUd3rD0TptwjisvE2TTnnlENtuY4/PZuoOYRiGp8Gw==}
/@rollup/rollup-linux-powerpc64le-gnu@4.17.2:
resolution: {integrity: sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==}
cpu: [ppc64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-riscv64-gnu@4.17.1:
resolution: {integrity: sha512-gEYmYYHaehdvX46mwXrU49vD6Euf1Bxhq9pPb82cbUU9UT2NV+RSckQ5tKWOnNXZixKsy8/cPGtiUWqzPuAcXQ==}
/@rollup/rollup-linux-riscv64-gnu@4.17.2:
resolution: {integrity: sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==}
cpu: [riscv64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-s390x-gnu@4.17.1:
resolution: {integrity: sha512-xeae5pMAxHFp6yX5vajInG2toST5lsCTrckSRUFwNgzYqnUjNBcQyqk1bXUxX5yhjWFl2Mnz3F8vQjl+2FRIcw==}
/@rollup/rollup-linux-s390x-gnu@4.17.2:
resolution: {integrity: sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==}
cpu: [s390x]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-gnu@4.17.1:
resolution: {integrity: sha512-AsdnINQoDWfKpBzCPqQWxSPdAWzSgnYbrJYtn6W0H2E9It5bZss99PiLA8CgmDRfvKygt20UpZ3xkhFlIfX9zQ==}
/@rollup/rollup-linux-x64-gnu@4.17.2:
resolution: {integrity: sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-linux-x64-musl@4.17.1:
resolution: {integrity: sha512-KoB4fyKXTR+wYENkIG3fFF+5G6N4GFvzYx8Jax8BR4vmddtuqSb5oQmYu2Uu067vT/Fod7gxeQYKupm8gAcMSQ==}
/@rollup/rollup-linux-x64-musl@4.17.2:
resolution: {integrity: sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==}
cpu: [x64]
os: [linux]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-arm64-msvc@4.17.1:
resolution: {integrity: sha512-J0d3NVNf7wBL9t4blCNat+d0PYqAx8wOoY+/9Q5cujnafbX7BmtYk3XvzkqLmFECaWvXGLuHmKj/wrILUinmQg==}
/@rollup/rollup-win32-arm64-msvc@4.17.2:
resolution: {integrity: sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==}
cpu: [arm64]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-ia32-msvc@4.17.1:
resolution: {integrity: sha512-xjgkWUwlq7IbgJSIxvl516FJ2iuC/7ttjsAxSPpC9kkI5iQQFHKyEN5BjbhvJ/IXIZ3yIBcW5QDlWAyrA+TFag==}
/@rollup/rollup-win32-ia32-msvc@4.17.2:
resolution: {integrity: sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==}
cpu: [ia32]
os: [win32]
requiresBuild: true
dev: true
optional: true
/@rollup/rollup-win32-x64-msvc@4.17.1:
resolution: {integrity: sha512-0QbCkfk6cnnVKWqqlC0cUrrUMDMfu5ffvYMTUHf+qMN2uAb3MKP31LPcwiMXBNsvoFGs/kYdFOsuLmvppCopXA==}
/@rollup/rollup-win32-x64-msvc@4.17.2:
resolution: {integrity: sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==}
cpu: [x64]
os: [win32]
requiresBuild: true
@ -1567,7 +1567,7 @@ packages:
resolution: {integrity: sha512-pvZdJ004TpC4Ohk9l0CxEXzS9E0L72b5n6lkIEIaWUIy/RlqnkDMHVtEC6InDjd4rt0jZKcvTrDKxeT96WUYnw==}
dependencies:
'@types/node': 20.12.7
'@types/vinyl': 2.0.11
'@types/vinyl': 2.0.12
dev: true
/@types/gulp@4.0.11:
@ -1700,13 +1700,6 @@ packages:
'@types/vinyl': 2.0.12
dev: true
/@types/vinyl@2.0.11:
resolution: {integrity: sha512-vPXzCLmRp74e9LsP8oltnWKTH+jBwt86WgRUb4Pc9Lf3pkMVGyvIo2gm9bODeGfCay2DBB/hAWDuvf07JcK4rw==}
dependencies:
'@types/expect': 1.20.4
'@types/node': 20.8.10
dev: true
/@types/vinyl@2.0.12:
resolution: {integrity: sha512-Sr2fYMBUVGYq8kj3UthXFAu5UN6ZW+rYr4NACjZQJvHvj+c8lYv0CahmZ2P/r7iUkN44gGUBwqxZkrKXYPb7cw==}
dependencies:
@ -1753,7 +1746,7 @@ packages:
/@vue/compiler-core@3.4.26:
resolution: {integrity: sha512-N9Vil6Hvw7NaiyFUFBPXrAyETIGlQ8KcFMkyk6hW1Cl6NvoqvP+Y8p1Eqvx+UdqsnrnI9+HMUEJegzia3mhXmQ==}
dependencies:
'@babel/parser': 7.24.4
'@babel/parser': 7.24.5
'@vue/shared': 3.4.26
entities: 4.5.0
estree-walker: 2.0.2
@ -1770,7 +1763,7 @@ packages:
/@vue/compiler-sfc@3.4.26:
resolution: {integrity: sha512-It1dp+FAOCgluYSVYlDn5DtZBxk1NCiJJfu2mlQqa/b+k8GL6NG/3/zRbJnHdhV2VhxFghaDq5L4K+1dakW6cw==}
dependencies:
'@babel/parser': 7.24.4
'@babel/parser': 7.24.5
'@vue/compiler-core': 3.4.26
'@vue/compiler-dom': 3.4.26
'@vue/compiler-ssr': 3.4.26
@ -2245,7 +2238,7 @@ packages:
resolution: {integrity: sha512-WKExI/eSGgGAkWAO+wMVdFObZV7hQen54UpD1kCCTN3tvlL3W1jL4+lPP/M7MwoP7Q4RHzKtO3JQ4HxYEcd+xQ==}
dependencies:
browserslist: 1.7.7
caniuse-db: 1.0.30001610
caniuse-db: 1.0.30001614
normalize-range: 0.1.2
num2fraction: 1.2.2
postcss: 5.2.18
@ -2330,8 +2323,8 @@ packages:
find-versions: 5.1.0
dev: true
/binary-extensions@2.2.0:
resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==}
/binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
dev: true
@ -2395,8 +2388,8 @@ packages:
deprecated: Browserslist 2 could fail on reading Browserslist >3.0 config used in other tools.
hasBin: true
dependencies:
caniuse-db: 1.0.30001610
electron-to-chromium: 1.4.736
caniuse-db: 1.0.30001614
electron-to-chromium: 1.4.751
dev: true
/browserslist@4.23.0:
@ -2480,13 +2473,13 @@ packages:
resolution: {integrity: sha512-SBTl70K0PkDUIebbkXrxWqZlHNs0wRgRD6QZ8guctShjbh63gEPfF+Wj0Yw+75f5Y8tSzqAI/NcisYv/cCah2Q==}
dependencies:
browserslist: 1.7.7
caniuse-db: 1.0.30001610
caniuse-db: 1.0.30001614
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
dev: true
/caniuse-db@1.0.30001610:
resolution: {integrity: sha512-GLdKwZR0S1EwOBIJlCze89enIbFf0Om/wzHF1GU8nIqf82NmxkB8JsfkOR5A6cZR57wiPJf0WBU+7gnNf0+EyQ==}
/caniuse-db@1.0.30001614:
resolution: {integrity: sha512-AVDISTXTRe7bHFnwAthHh1tNe3FRRap/Ce8rPw+kmNY0k4fV5RVtxvDudXoIyTjfOtRt0Jf7gZvQuBfsjLZI3A==}
dev: true
/caniuse-lite@1.0.30001606:
@ -2975,7 +2968,7 @@ packages:
resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
engines: {node: '>=0.11'}
dependencies:
'@babel/runtime': 7.24.4
'@babel/runtime': 7.24.5
dev: true
/dayjs@1.11.11:
@ -3086,8 +3079,8 @@ packages:
resolution: {integrity: sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA==}
dev: true
/electron-to-chromium@1.4.736:
resolution: {integrity: sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==}
/electron-to-chromium@1.4.751:
resolution: {integrity: sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==}
dev: true
/emoji-regex@8.0.0:
@ -3854,7 +3847,7 @@ packages:
engines: {node: '>=10'}
dependencies:
'@types/node': 20.12.7
'@types/vinyl': 2.0.11
'@types/vinyl': 2.0.12
istextorbinary: 3.3.0
replacestream: 4.0.3
yargs-parser: 21.1.1
@ -3865,7 +3858,7 @@ packages:
engines: {node: '>=10'}
dependencies:
plugin-error: 1.0.1
terser: 5.30.3
terser: 5.31.0
through2: 4.0.2
vinyl-sourcemaps-apply: 0.2.1
dev: true
@ -4106,7 +4099,7 @@ packages:
resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==}
engines: {node: '>=8'}
dependencies:
binary-extensions: 2.2.0
binary-extensions: 2.3.0
dev: true
/is-ci@3.0.1:
@ -5596,29 +5589,29 @@ packages:
seedrandom: 2.4.2
dev: true
/rollup@4.17.1:
resolution: {integrity: sha512-0gG94inrUtg25sB2V/pApwiv1lUb0bQ25FPNuzO89Baa+B+c0ccaaBKM5zkZV/12pUUdH+lWCSm9wmHqyocuVQ==}
/rollup@4.17.2:
resolution: {integrity: sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
dependencies:
'@types/estree': 1.0.5
optionalDependencies:
'@rollup/rollup-android-arm-eabi': 4.17.1
'@rollup/rollup-android-arm64': 4.17.1
'@rollup/rollup-darwin-arm64': 4.17.1
'@rollup/rollup-darwin-x64': 4.17.1
'@rollup/rollup-linux-arm-gnueabihf': 4.17.1
'@rollup/rollup-linux-arm-musleabihf': 4.17.1
'@rollup/rollup-linux-arm64-gnu': 4.17.1
'@rollup/rollup-linux-arm64-musl': 4.17.1
'@rollup/rollup-linux-powerpc64le-gnu': 4.17.1
'@rollup/rollup-linux-riscv64-gnu': 4.17.1
'@rollup/rollup-linux-s390x-gnu': 4.17.1
'@rollup/rollup-linux-x64-gnu': 4.17.1
'@rollup/rollup-linux-x64-musl': 4.17.1
'@rollup/rollup-win32-arm64-msvc': 4.17.1
'@rollup/rollup-win32-ia32-msvc': 4.17.1
'@rollup/rollup-win32-x64-msvc': 4.17.1
'@rollup/rollup-android-arm-eabi': 4.17.2
'@rollup/rollup-android-arm64': 4.17.2
'@rollup/rollup-darwin-arm64': 4.17.2
'@rollup/rollup-darwin-x64': 4.17.2
'@rollup/rollup-linux-arm-gnueabihf': 4.17.2
'@rollup/rollup-linux-arm-musleabihf': 4.17.2
'@rollup/rollup-linux-arm64-gnu': 4.17.2
'@rollup/rollup-linux-arm64-musl': 4.17.2
'@rollup/rollup-linux-powerpc64le-gnu': 4.17.2
'@rollup/rollup-linux-riscv64-gnu': 4.17.2
'@rollup/rollup-linux-s390x-gnu': 4.17.2
'@rollup/rollup-linux-x64-gnu': 4.17.2
'@rollup/rollup-linux-x64-musl': 4.17.2
'@rollup/rollup-win32-arm64-msvc': 4.17.2
'@rollup/rollup-win32-ia32-msvc': 4.17.2
'@rollup/rollup-win32-x64-msvc': 4.17.2
fsevents: 2.3.3
dev: true
@ -6187,6 +6180,17 @@ packages:
source-map-support: 0.5.21
dev: true
/terser@5.31.0:
resolution: {integrity: sha512-Q1JFAoUKE5IMfI4Z/lkE/E6+SwgzO+x4tq4v1AyBLRj8VSYvRO6A/rQrPg1yud4g0En9EKI1TvFRF2tQFcoUkg==}
engines: {node: '>=10'}
hasBin: true
dependencies:
'@jridgewell/source-map': 0.3.6
acorn: 8.11.3
commander: 2.20.3
source-map-support: 0.5.21
dev: true
/textarea-caret@3.1.0:
resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==}
dev: true
@ -6723,14 +6727,14 @@ packages:
'@types/node': 20.12.7
esbuild: 0.20.2
postcss: 8.4.38
rollup: 4.17.1
rollup: 4.17.2
sass: 1.62.1
optionalDependencies:
fsevents: 2.3.3
dev: true
/vue-component-type-helpers@2.0.14:
resolution: {integrity: sha512-DInfgOyXlMyliyqAAD9frK28tTfch0+tMi4qoWJcZlRxUf+NFAtraJBnAsKLep+FOyLMiajkhfyEb3xLK08i7w==}
/vue-component-type-helpers@2.0.15:
resolution: {integrity: sha512-jR/Hw52gzNQxMovJBsOQ/F9E1UQ8K1Np0CVG3RnueLkaCKqWuyL9XHl/5tUBAGJx+bk5xZ+co7vK23+Pzt75Lg==}
dev: true
/vue-demi@0.14.7(vue@3.4.26):

View File

@ -122,12 +122,11 @@ async fn main() -> miette::Result<()> {
let listener = TcpListener::bind(addr).await.into_diagnostic()?;
tracing::info!("Serving...");
axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.map_err(|e| miette!("Error running server: {}", e))
}
// FIXME: Plug this back in when Axum reimplements graceful shutdown
async fn shutdown_signal() {
let ctrl_c = async {
if let Err(e) = signal::ctrl_c().await {

View File

@ -93,11 +93,11 @@ async fn main() -> miette::Result<()> {
let listener = TcpListener::bind(addr).await.into_diagnostic()?;
info!("Serving...");
axum::serve(listener, app.into_make_service())
.with_graceful_shutdown(shutdown_signal())
.await
.map_err(|e| miette!("Error running server: {}", e))
}
// FIXME: Plug this back in when Axum reimplements graceful shutdown
async fn shutdown_signal() {
let ctrl_c = async {
if let Err(e) = signal::ctrl_c().await {

View File

@ -23,6 +23,21 @@ pub enum UserCacheError {
RedisError(#[from] CalckeyCacheError),
}
#[derive(Debug, Clone)]
pub struct CachedLocalUser {
pub user: Arc<ck::user::Model>,
pub profile: Arc<ck::user_profile::Model>,
}
impl From<(ck::user::Model, ck::user_profile::Model)> for CachedLocalUser {
fn from((user, profile): (ck::user::Model, ck::user_profile::Model)) -> Self {
CachedLocalUser {
user: Arc::new(user),
profile: Arc::new(profile),
}
}
}
impl From<UserCacheError> for ApiError {
fn from(err: UserCacheError) -> Self {
let mut api_error: ApiError = match err {
@ -38,44 +53,42 @@ impl From<UserCacheError> for ApiError {
struct LocalUserCache {
lifetime: TimedCache<String, ()>,
id_to_user: HashMap<String, Arc<ck::user::Model>>,
token_to_user: HashMap<String, Arc<ck::user::Model>>,
id_to_user: HashMap<String, CachedLocalUser>,
token_to_user: HashMap<String, CachedLocalUser>,
}
impl LocalUserCache {
fn purge(&mut self, user: impl AsRef<ck::user::Model>) {
let user = user.as_ref();
fn purge(&mut self, user: &CachedLocalUser) {
self.lifetime.cache_remove(&user.user.id);
self.lifetime.cache_remove(&user.id);
if let Some(user) = self.id_to_user.remove(&user.id) {
if let Some(token) = user.token.clone() {
if let Some(user) = self.id_to_user.remove(&user.user.id) {
if let Some(token) = user.user.token.clone() {
self.token_to_user.remove(&token);
}
}
}
fn refresh(&mut self, user: Arc<ck::user::Model>) {
self.purge(&user);
fn refresh(&mut self, user: &CachedLocalUser) {
self.purge(user);
self.lifetime.cache_set(user.id.clone(), ());
self.lifetime.cache_set(user.user.id.clone(), ());
self.id_to_user.insert(user.id.clone(), user.clone());
self.id_to_user.insert(user.user.id.clone(), user.clone());
if let Some(token) = user.token.clone() {
if let Some(token) = user.user.token.clone() {
self.token_to_user.insert(token, user.clone());
}
}
/// Low-priority refresh. Only refreshes the cache if the user is not there.
/// Used mostly for getters that would otherwise data race with more important refreshes.
fn maybe_refresh(&mut self, user: &Arc<ck::user::Model>) {
if self.lifetime.cache_get(&user.id).is_none() {
self.refresh(user.clone());
fn maybe_refresh(&mut self, user: &CachedLocalUser) {
if self.lifetime.cache_get(&user.user.id).is_none() {
self.refresh(user);
}
}
fn get_by_id(&mut self, id: &str) -> Option<Arc<ck::user::Model>> {
fn get_by_id(&mut self, id: &str) -> Option<CachedLocalUser> {
if let Some(user) = self.id_to_user.get(id).cloned() {
if self.lifetime.cache_get(id).is_none() {
self.purge(&user);
@ -88,9 +101,9 @@ impl LocalUserCache {
None
}
fn get_by_token(&mut self, token: &str) -> Option<Arc<ck::user::Model>> {
fn get_by_token(&mut self, token: &str) -> Option<CachedLocalUser> {
if let Some(user) = self.token_to_user.get(token).cloned() {
if self.lifetime.cache_get(&user.id).is_none() {
if self.lifetime.cache_get(&user.user.id).is_none() {
self.purge(&user);
return None;
}
@ -143,8 +156,8 @@ impl LocalUserCacheService {
| InternalStreamMessage::UserChangeSuspendedState { id, .. }
| InternalStreamMessage::RemoteUserUpdated { id }
| InternalStreamMessage::UserTokenRegenerated { id, .. } => {
let user = match db.get_user_by_id(&id).await {
Ok(Some(user)) => user,
let user_profile = match db.get_user_and_profile_by_id(&id).await {
Ok(Some(m)) => m,
Ok(None) => return,
Err(e) => {
error!("Error fetching user from database: {}", e);
@ -152,7 +165,7 @@ impl LocalUserCacheService {
}
};
cache.lock().await.refresh(Arc::new(user));
cache.lock().await.refresh(&CachedLocalUser::from(user_profile));
}
_ => {}
};
@ -169,10 +182,9 @@ impl LocalUserCacheService {
async fn map_cache_user(
&self,
user: Option<ck::user::Model>,
) -> Result<Option<Arc<ck::user::Model>>, UserCacheError> {
user: Option<CachedLocalUser>,
) -> Result<Option<CachedLocalUser>, UserCacheError> {
if let Some(user) = user {
let user = Arc::new(user);
self.cache.lock().await.maybe_refresh(&user);
return Ok(Some(user));
}
@ -183,29 +195,27 @@ impl LocalUserCacheService {
pub async fn get_by_token(
&self,
token: &str,
) -> Result<Option<Arc<ck::user::Model>>, UserCacheError> {
) -> Result<Option<CachedLocalUser>, UserCacheError> {
let result = self.cache.lock().await.get_by_token(token);
if let Some(user) = result {
return Ok(Some(user));
}
self.map_cache_user(self.db.get_user_by_token(token).await?)
self.map_cache_user(self.db.get_user_and_profile_by_token(token).await?.map(CachedLocalUser::from))
.await
}
pub async fn get_by_id(
&self,
id: &str,
) -> Result<Option<Arc<ck::user::Model>>, UserCacheError> {
) -> Result<Option<CachedLocalUser>, UserCacheError> {
let result = self.cache.lock().await.get_by_id(id);
if let Some(user) = result {
return Ok(Some(user));
}
let user = self.db.get_user_by_id(id).await?;
self.map_cache_user(user).await
self.map_cache_user(self.db.get_user_and_profile_by_id(id).await?.map(CachedLocalUser::from)).await
}
}

View File

@ -1,22 +1,25 @@
use crate::service::local_user_cache::UserCacheError;
use crate::service::MagnetarService;
use crate::web::{ApiError, IntoErrorCode};
use axum::async_trait;
use axum::extract::rejection::ExtensionRejection;
use axum::extract::{FromRequestParts, Request, State};
use axum::http::request::Parts;
use axum::http::{HeaderMap, StatusCode};
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use headers::authorization::Bearer;
use headers::{Authorization, HeaderMapExt};
use magnetar_model::{ck, CalckeyDbError};
use std::convert::Infallible;
use std::sync::Arc;
use axum::async_trait;
use axum::extract::{FromRequestParts, Request, State};
use axum::extract::rejection::ExtensionRejection;
use axum::http::{HeaderMap, StatusCode};
use axum::http::request::Parts;
use axum::middleware::Next;
use axum::response::{IntoResponse, Response};
use headers::{Authorization, HeaderMapExt};
use headers::authorization::Bearer;
use strum::IntoStaticStr;
use thiserror::Error;
use tracing::error;
use magnetar_model::{CalckeyDbError, ck};
use crate::service::local_user_cache::{CachedLocalUser, UserCacheError};
use crate::service::MagnetarService;
use crate::web::{ApiError, IntoErrorCode};
#[derive(Clone, Debug)]
pub enum AuthMode {
User {
@ -178,7 +181,7 @@ impl AuthState {
let user_cache = &self.service.local_user_cache;
let user = user_cache.get_by_token(token).await?;
if let Some(user) = user {
if let Some(CachedLocalUser { user, .. }) = user {
return Ok(AuthMode::User { user });
}
@ -205,7 +208,7 @@ impl AuthState {
});
}
let user = user.unwrap();
let CachedLocalUser { user, .. } = user.unwrap();
if let Some(app_id) = &access_token.app_id {
return match self.service.db.get_app_by_id(app_id).await? {