Merge branch 'development'
ci/woodpecker/push/ociImagePush Pipeline failed Details

# Conflicts:
#	fe_calckey/frontend/package.json
This commit is contained in:
Natty 2023-11-05 20:41:00 +01:00
commit fdfd3163b1
Signed by: natty
GPG Key ID: BF6CB659ADEE60EC
159 changed files with 3919 additions and 901 deletions

View File

@ -1,2 +1,2 @@
[registries.crates-io] [registries.crates-io]
protocol = "sparse" protocol = "sparse"

31
Cargo.lock generated
View File

@ -535,7 +535,7 @@ dependencies = [
[[package]] [[package]]
name = "ck" name = "ck"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"sea-orm", "sea-orm",
"serde", "serde",
@ -835,7 +835,7 @@ checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]] [[package]]
name = "ext_calckey_model_migration" name = "ext_calckey_model_migration"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"sea-orm-migration", "sea-orm-migration",
"tokio", "tokio",
@ -1446,7 +1446,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar" name = "magnetar"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"axum", "axum",
"cached", "cached",
@ -1482,11 +1482,12 @@ dependencies = [
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
"unicode-segmentation", "unicode-segmentation",
"url",
] ]
[[package]] [[package]]
name = "magnetar_calckey_fe" name = "magnetar_calckey_fe"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@ -1510,7 +1511,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_calckey_model" name = "magnetar_calckey_model"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"chrono", "chrono",
"ck", "ck",
@ -1535,19 +1536,21 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_common" name = "magnetar_common"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"idna",
"magnetar_core", "magnetar_core",
"magnetar_sdk", "magnetar_sdk",
"percent-encoding", "percent-encoding",
"serde", "serde",
"thiserror", "thiserror",
"toml 0.8.1", "toml 0.8.1",
"url",
] ]
[[package]] [[package]]
name = "magnetar_core" name = "magnetar_core"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -1555,7 +1558,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_mmm_parser" name = "magnetar_mmm_parser"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"compact_str", "compact_str",
"either", "either",
@ -1571,7 +1574,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_nodeinfo" name = "magnetar_nodeinfo"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
@ -1579,7 +1582,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_sdk" name = "magnetar_sdk"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"chrono", "chrono",
"http", "http",
@ -1593,7 +1596,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_sdk_macros" name = "magnetar_sdk_macros"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.28", "syn 2.0.28",
@ -1601,7 +1604,7 @@ dependencies = [
[[package]] [[package]]
name = "magnetar_webfinger" name = "magnetar_webfinger"
version = "0.2.1-alpha" version = "0.3.0-alpha"
dependencies = [ dependencies = [
"magnetar_core", "magnetar_core",
"serde", "serde",
@ -3670,9 +3673,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]] [[package]]
name = "url" name = "url"
version = "2.4.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",

View File

@ -19,7 +19,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.2.1-alpha" version = "0.3.0-alpha"
edition = "2021" edition = "2021"
[workspace.dependencies] [workspace.dependencies]
@ -88,6 +88,7 @@ hyper = { workspace = true, features = ["full"] }
tokio = { workspace = true, features = ["full"] } tokio = { workspace = true, features = ["full"] }
tower = { workspace = true } tower = { workspace = true }
tower-http = { workspace = true, features = ["cors", "trace", "fs"] } tower-http = { workspace = true, features = ["cors", "trace", "fs"] }
url = { workspace = true }
idna = { workspace = true } idna = { workspace = true }

View File

@ -47,6 +47,19 @@
# Environment variable: MAG_C_BIND_ADDR # Environment variable: MAG_C_BIND_ADDR
# networking.bind_addr = "::" # networking.bind_addr = "::"
# [Optional]
# The URL of a media proxy
# Default: null
# Environment variable: MAG_C_MEDIA_PROXY
# networking.media_proxy = ""
# [Optional]
# Whether to proxy remote files through this instance
# Default: false
# Environment variable: MAG_C_PROXY_REMOTE_FILES
# networking.proxy_remote_files = false
# -----------------------------[ CALCKEY FRONTEND ]---------------------------- # -----------------------------[ CALCKEY FRONTEND ]----------------------------
# [Optional] # [Optional]

View File

@ -61,6 +61,7 @@ impl CalckeyModel {
.sqlx_logging_level(LevelFilter::Debug) .sqlx_logging_level(LevelFilter::Debug)
.to_owned(); .to_owned();
info!("Attempting database connection...");
Ok(CalckeyModel(sea_orm::Database::connect(opt).await?)) Ok(CalckeyModel(sea_orm::Database::connect(opt).await?))
} }
@ -224,6 +225,18 @@ impl CalckeyModel {
.await?) .await?)
} }
pub async fn get_instance(
&self,
host: &str,
) -> Result<Option<instance::Model>, CalckeyDbError> {
let instance = instance::Entity::find()
.filter(instance::Column::Host.eq(host))
.one(&self.0)
.await?;
Ok(instance)
}
pub async fn get_instance_meta(&self) -> Result<meta::Model, CalckeyDbError> { pub async fn get_instance_meta(&self) -> Result<meta::Model, CalckeyDbError> {
let txn = self.0.begin().await?; let txn = self.0.begin().await?;

View File

@ -5,7 +5,6 @@ use sea_orm::{
QueryFilter, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select, QueryFilter, QueryResult, QuerySelect, QueryTrait, RelationTrait, Select,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::info;
use ck::{drive_file, note, note_reaction, user}; use ck::{drive_file, note, note_reaction, user};
use magnetar_sdk::types::RangeFilter; use magnetar_sdk::types::RangeFilter;

View File

@ -15,7 +15,7 @@
"devDependencies": { "devDependencies": {
"@swc/cli": "^0.1.62", "@swc/cli": "^0.1.62",
"@swc/core": "^1.3.62", "@swc/core": "^1.3.62",
"@types/node": "20.3.1", "@types/node": "20.8.10",
"ts-node": "10.4.0", "ts-node": "10.4.0",
"tsd": "^0.28.1", "tsd": "^0.28.1",
"typescript": "5.1.3" "typescript": "5.1.3"

View File

@ -15,6 +15,8 @@ export type UserLite = {
avatarUrl: string; avatarUrl: string;
avatarBlurhash: string; avatarBlurhash: string;
alsoKnownAs: string[]; alsoKnownAs: string[];
isCat?: boolean;
isBot?: boolean;
movedToUri: any; movedToUri: any;
emojis: { emojis: {
name: string; name: string;

View File

@ -0,0 +1,937 @@
<template>
<div
:aria-label="accessibleLabel"
v-if="!muted.muted"
v-show="!isDeleted"
ref="el"
v-hotkey="keymap"
v-size="{ max: [500, 350] }"
class="tkcbzcuz note-container"
:tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }"
:id="appearNote.id"
>
<MkNoteSub
v-if="appearNote.parent_note && !detailedView && !collapsedReply"
:note="appearNote.parent_note"
class="reply-to"
/>
<div
v-if="!detailedView"
class="note-context"
@click="noteClick"
:class="{
collapsedReply: collapsedReply && appearNote.parent_note,
}"
>
<div class="line"></div>
<div v-if="appearNote._prId_" class="info">
<i class="ph-megaphone-simple-bold ph-lg"></i>
{{ i18n.ts.promotion
}}<button class="_textButton hide" @click.stop="readPromo()">
{{ i18n.ts.hideThisNote }}
<i class="ph-x ph-bold ph-lg"></i>
</button>
</div>
<div v-if="appearNote._featuredId_" class="info">
<i class="ph-lightning ph-bold ph-lg"></i>
{{ i18n.ts.featured }}
</div>
<div v-if="pinned" class="info">
<i class="ph-push-pin ph-bold ph-lg"></i
>{{ i18n.ts.pinnedNote }}
</div>
<div v-if="isRenote" class="renote">
<i class="ph-repeat ph-bold ph-lg"></i>
<I18n :src="i18n.ts.renotedBy" tag="span">
<template #user>
<MkA
v-user-preview="note.user.id"
class="name"
:to="userPage(note.user)"
@click.stop
>
<MkUserName :user="note.user" />
</MkA>
</template>
</I18n>
<div class="info">
<button
ref="renoteTime"
class="_button time"
@click.stop="showRenoteMenu()"
>
<i
v-if="isMyRenote"
class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"
></i>
<MkTime :time="note.created_at" />
</button>
<MkVisibility :note="note" />
</div>
</div>
<div v-if="collapsedReply && appearNote.parent_note" class="info">
<MkAvatar class="avatar" :user="appearNote.parent_note.user" />
<MkUserName
class="username"
:user="appearNote.parent_note.user"
></MkUserName>
<Mfm
class="summary"
:text="getNoteSummary(appearNote.parent_note)"
:plain="true"
:nowrap="true"
:custom-emojis="note.emojis"
/>
</div>
</div>
<article
class="article"
@contextmenu.stop="onContextmenu"
@click="noteClick"
:style="{
cursor: expandOnNoteClick && !detailedView ? 'pointer' : '',
}"
>
<div class="main">
<div class="header-container">
<MkAvatar class="avatar" :user="appearNote.user" />
<XNoteHeader class="header" :note="appearNote" />
</div>
<div class="body">
<MkSubNoteContent
class="text"
:note="appearNote"
:detailed="true"
:detailedView="detailedView"
:parentId="appearNote.id"
@push="(e) => router.push(notePage(e))"
@focusfooter="footerEl.focus()"
@expanded="(e) => setPostExpanded(e)"
></MkSubNoteContent>
<div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini />
<div v-else class="translated">
<b
>{{
i18n.t("translatedFrom", {
x: translation.sourceLang,
})
}}:
</b>
<Mfm
:text="translation.text"
:author="appearNote.user"
:i="$i"
:custom-emojis="appearNote.emojis"
/>
</div>
</div>
</div>
<div v-if="detailedView" class="info">
<MkA class="created-at" :to="notePage(appearNote)">
<MkTime :time="appearNote.created_at" mode="absolute" />
</MkA>
</div>
<footer ref="footerEl" class="footer" @click.stop tabindex="-1">
<XReactionsViewer
v-if="enableEmojiReactions"
ref="reactionsViewer"
:note="appearNote"
/>
<button
v-tooltip.noDelay.bottom="i18n.ts.reply"
class="button _button"
@click="reply()"
>
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<template
v-if="appearNote.reply_count > 0 && !detailedView"
>
<p class="count">{{ appearNote.reply_count }}</p>
</template>
</button>
<XRenoteButton
ref="renoteButton"
class="button"
:note="appearNote"
:count="appearNote.renote_count"
:detailedView="detailedView"
/>
<XStarButtonNoEmoji
v-if="!enableEmojiReactions"
class="button"
:note="appearNote"
:count="magReactionCount(appearNote)"
:reacted="magHasReacted(appearNote)"
/>
<XStarButton
v-if="
enableEmojiReactions && !magHasReacted(appearNote)
"
ref="starButton"
class="button"
:note="appearNote"
/>
<button
v-if="
enableEmojiReactions && !magHasReacted(appearNote)
"
ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction"
class="button _button"
@click="react()"
>
<i class="ph-smiley ph-bold ph-lg"></i>
</button>
<button
v-if="enableEmojiReactions && magHasReacted(appearNote)"
ref="reactButton"
class="button _button reacted"
@click="undoReact(appearNote)"
v-tooltip.noDelay.bottom="i18n.ts.removeReaction"
>
<i class="ph-minus ph-bold ph-lg"></i>
</button>
<XQuoteButton class="button" :note="appearNote" />
<button
ref="menuButton"
v-tooltip.noDelay.bottom="i18n.ts.more"
class="button _button"
@click="menu()"
>
<i class="ph-dots-three-outline ph-bold ph-lg"></i>
</button>
</footer>
</div>
</article>
</div>
<button v-else class="muted _button" @click="muted.muted = false">
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
<template #name>
<MkA
v-user-preview="note.user.id"
class="name"
:to="userPage(note.user)"
>
<MkUserName :user="note.user" />
</MkA>
</template>
<template #reason>
<b class="_blur_text">{{ muted.matched.join(", ") }}</b>
</template>
</I18n>
</button>
</template>
<script lang="ts" setup>
import type { Ref } from "vue";
import { computed, inject, onMounted, ref, toRaw } from "vue";
import * as mfm from "mfm-js";
import type * as misskey from "calckey-js";
import MkNoteSub from "@/components/MkNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue";
import MkVisibility from "@/components/MkVisibility.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config";
import { pleaseLogin } from "@/scripts/please-login";
import { focusNext, focusPrev } from "@/scripts/focus";
import { getWordSoftMute } from "@/scripts/check-word-mute";
import { useRouter } from "@/router";
import { userPage } from "@/filters/user";
import * as os from "@/os";
import { defaultStore, noteViewInterruptors } from "@/store";
import { reactionPicker } from "@/scripts/reaction-picker";
import { $i } from "@/account";
import { i18n } from "@/i18n";
import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture";
import { notePage } from "@/filters/note";
import { getNoteSummary } from "@/scripts/get-note-summary";
import { packed } from "magnetar-common";
import {
magEffectiveNote,
magHasReacted,
magIsRenote,
magReactionCount,
} from "@/scripts-mag/mag-util";
const router = useRouter();
const props = defineProps<{
note: packed.PackNoteMaybeFull;
pinned?: boolean;
detailedView?: boolean;
collapsedReply?: boolean;
}>();
let note = $ref<packed.PackNoteMaybeFull>(structuredClone(toRaw(props.note)));
const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason;
if (what === "reply") return i18n.ts.userSaysSomethingReasonReply;
if (what === "renote") return i18n.ts.userSaysSomethingReasonRenote;
if (what === "quote") return i18n.ts.userSaysSomethingReasonQuote;
// I don't think here is reachable, but just in case
return i18n.ts.userSaysSomething;
};
// plugin
if (noteViewInterruptors.length > 0) {
onMounted(async () => {
let result = structuredClone(toRaw(note));
for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result);
}
note = result;
});
}
const isRenote = magIsRenote(note);
const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement>();
let appearNote = $computed(
() => magEffectiveNote(note) as packed.PackNoteMaybeFull
);
const isMyRenote = $i && $i.id === note.user.id;
const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
const translation = ref(null);
const translating = ref(false);
const enableEmojiReactions = defaultStore.state.enableEmojiReactions;
const expandOnNoteClick = defaultStore.state.expandOnNoteClick;
const keymap = {
r: () => reply(true),
"e|a|plus": () => react(true),
q: () => renoteButton.value.renote(true),
"up|k": focusBefore,
"down|j": focusAfter,
esc: blur,
"m|o": () => menu(true),
s: () => showContent.value !== showContent.value,
};
useNoteCapture({
rootEl: el,
note: $$(appearNote),
isDeletedRef: isDeleted,
});
function reply(viaKeyboard = false): void {
pleaseLogin();
os.post(
{
reply: appearNote,
animation: !viaKeyboard,
},
() => {
focus();
}
);
}
function react(viaKeyboard = false): void {
pleaseLogin();
blur();
reactionPicker.show(
reactButton.value,
(reaction) => {
os.api("notes/reactions/create", {
noteId: appearNote.id,
reaction: reaction,
});
},
() => {
focus();
}
);
}
function undoReact(note): void {
const oldReaction = note.myReaction;
if (!oldReaction) return;
os.api("notes/reactions/delete", {
noteId: note.id,
});
}
const currentClipPage = inject<Ref<misskey.entities.Clip> | null>(
"currentClipPage",
null
);
function onContextmenu(ev: MouseEvent): void {
const isLink = (el: HTMLElement) => {
if (el.tagName === "A") return true;
// The Audio element's context menu is the browser default, such as for selecting playback speed.
if (el.tagName === "AUDIO") return true;
if (el.parentElement) {
return isLink(el.parentElement);
}
};
if (isLink(ev.target)) return;
if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault();
react();
} else {
os.contextMenu(
[
{
type: "label",
text: notePage(appearNote),
},
{
icon: "ph-browser ph-bold ph-lg",
text: i18n.ts.openInWindow,
action: () => {
os.pageWindow(notePage(appearNote));
},
},
notePage(appearNote) != location.pathname
? {
icon: "ph-arrows-out-simple ph-bold ph-lg",
text: i18n.ts.showInPage,
action: () => {
router.push(notePage(appearNote), "forcePage");
},
}
: undefined,
null,
{
type: "a",
icon: "ph-arrow-square-out ph-bold ph-lg",
text: i18n.ts.openInNewTab,
href: notePage(appearNote),
target: "_blank",
},
{
icon: "ph-link-simple ph-bold ph-lg",
text: i18n.ts.copyLink,
action: () => {
copyToClipboard(`${url}${notePage(appearNote)}`);
},
},
appearNote.user.host != null
? {
type: "a",
icon: "ph-arrow-square-up-right ph-bold ph-lg",
text: i18n.ts.showOnRemote,
href: appearNote.url ?? appearNote.uri ?? "",
target: "_blank",
}
: undefined,
],
ev
);
}
}
function menu(viaKeyboard = false): void {
os.popupMenu(
getNoteMenu({
note: note,
translating,
translation,
menuButton,
isDeleted,
currentClipPage,
}),
menuButton.value,
{
viaKeyboard,
}
).then(focus);
}
function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return;
os.popupMenu(
[
{
text: i18n.ts.unrenote,
icon: "ph-trash ph-bold ph-lg",
danger: true,
action: () => {
os.api("notes/delete", {
noteId: note.id,
});
isDeleted.value = true;
},
},
],
renoteTime.value,
{
viaKeyboard: viaKeyboard,
}
);
}
function focus() {
el.value?.focus();
}
function blur() {
el.value?.blur();
}
function focusBefore() {
focusPrev(el.value);
}
function focusAfter() {
focusNext(el.value);
}
function scrollIntoView() {
el.value?.scrollIntoView();
}
function noteClick(e) {
if (
document.getSelection()?.type === "Range" ||
props.detailedView ||
!expandOnNoteClick
) {
e.stopPropagation();
} else {
router.push(notePage(appearNote));
}
}
function readPromo() {
os.api("promo/read", {
noteId: appearNote.id,
});
isDeleted.value = true;
}
let postIsExpanded = ref(false);
function setPostExpanded(val: boolean) {
postIsExpanded.value = val;
}
const accessibleLabel = computed(() => {
let label = `${appearNote.user.username}; `;
if (appearNote.renoted_note) {
label += `${i18n.t("renoted")} ${
appearNote.renoted_note.user.username
}; `;
if (appearNote.renoted_note.cw) {
label += `${i18n.t("cw")}: ${appearNote.renoted_note.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.renoted_note.text}; `;
}
} else {
label += `${appearNote.renoted_note.text}; `;
}
} else {
if (appearNote.cw) {
label += `${i18n.t("cw")}: ${appearNote.cw}; `;
if (postIsExpanded.value) {
label += `${appearNote.text}; `;
}
} else {
label += `${appearNote.text}; `;
}
}
const date = new Date(appearNote.created_at);
label += `${date.toLocaleTimeString()}`;
return label;
});
defineExpose({
focus,
blur,
scrollIntoView,
});
</script>
<style lang="scss" scoped>
.tkcbzcuz {
position: relative;
transition: box-shadow 0.1s ease;
font-size: 1.05em;
overflow: clip;
contain: content;
-webkit-tap-highlight-color: transparent;
//
//
// contain-intrinsic-size
//
// ()
//content-visibility: auto;
//contain-intrinsic-size: 0 128px;
&:focus-visible {
outline: none;
&:after {
content: "";
pointer-events: none;
display: block;
position: absolute;
z-index: 10;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
border: solid 1px var(--focus);
border-radius: var(--radius);
box-sizing: border-box;
}
}
& > .article > .main {
&:hover,
&:focus-within {
:deep(.footer .button) {
opacity: 1;
}
}
}
> .reply-to {
& + .note-context {
.line::before {
content: "";
display: block;
margin-bottom: -4px;
margin-top: 16px;
border-left: 2px solid currentColor;
margin-left: calc((var(--avatarSize) / 2) - 1px);
opacity: 0.25;
}
}
}
.note-context {
position: relative;
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;
width: var(--avatarSize);
display: flex;
margin-right: 14px;
margin-top: 0;
flex-grow: 0;
pointer-events: none;
}
> div > i {
margin-left: -0.5px;
}
> .info {
display: flex;
align-items: center;
font-size: 90%;
white-space: pre;
color: #f6c177;
> i {
margin-right: 4px;
}
> .hide {
margin-left: auto;
color: inherit;
}
}
> .renote {
display: flex;
align-items: center;
white-space: pre;
color: var(--renote);
cursor: pointer;
> i {
margin-right: 4px;
}
> span {
overflow: hidden;
flex-shrink: 1;
text-overflow: ellipsis;
white-space: nowrap;
> .name {
font-weight: bold;
}
}
> .info {
margin-left: auto;
font-size: 0.9em;
display: flex;
> .time {
flex-shrink: 0;
color: inherit;
display: inline-flex;
align-items: center;
> .dropdownIcon {
margin-right: 4px;
}
}
}
}
&.collapsedReply {
.line {
opacity: 0.25;
&::after {
content: "";
position: absolute;
border-left: 2px solid currentColor;
border-top: 2px solid currentColor;
margin-left: calc(var(--avatarSize) / 2 - 1px);
width: calc(var(--avatarSize) / 2 + 14px);
border-top-left-radius: calc(var(--avatarSize) / 4);
top: calc(50% - 1px);
height: calc(50% + 5px);
}
}
.info {
color: var(--fgTransparentWeak);
transition: color 0.2s;
}
.avatar {
width: 1.2em;
height: 1.2em;
border-radius: 2em;
overflow: hidden;
margin-right: 0.4em;
background: var(--panelHighlight);
}
.username {
font-weight: 700;
flex-shrink: 0;
max-width: 30%;
&::after {
content: ": ";
}
}
&:hover,
&:focus-within {
.info {
color: var(--fg);
}
}
}
}
> .article {
position: relative;
overflow: clip;
padding: 20px 32px 10px;
margin-top: -16px;
&:first-child,
&:nth-child(2) {
margin-top: -100px;
padding-top: 104px;
}
@media (pointer: coarse) {
cursor: default;
}
.header-container {
display: flex;
position: relative;
z-index: 2;
> .avatar {
flex-shrink: 0;
display: block;
margin: 0 14px 0 0;
width: var(--avatarSize);
height: var(--avatarSize);
position: relative;
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);
}
}
}
}
> .info {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
gap: 0.7em;
margin-top: 16px;
opacity: 0.7;
font-size: 0.9em;
}
> .footer {
position: relative;
z-index: 2;
display: flex;
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;
padding: 8px;
opacity: 0.7;
flex-grow: 1;
max-width: 3.5em;
width: max-content;
min-width: max-content;
pointer-events: all;
height: auto;
transition: opacity 0.2s;
&::before {
content: "";
position: absolute;
inset: 0;
bottom: 2px;
background: var(--panel);
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);
}
> i {
display: inline !important;
}
> .count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
&.reacted {
color: var(--accent);
}
}
}
}
}
> .reply {
border-top: solid 0.5px var(--divider);
}
&.max-width_500px {
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));
}
}
}
&.max-width_300px {
--avatarSize: 40px;
}
}
.muted {
padding: 8px;
text-align: center;
opacity: 0.7;
width: 100%;
._blur_text {
pointer-events: auto;
}
}
</style>

View File

@ -7,7 +7,7 @@
v-size="{ max: [500, 350, 300] }" v-size="{ max: [500, 350, 300] }"
class="lxwezrsl _block" class="lxwezrsl _block"
:tabindex="!isDeleted ? '-1' : null" :tabindex="!isDeleted ? '-1' : null"
:class="{ renote: isRenote }" :class="{ renote: magIsRenote(note) }"
> >
<MkNoteSub <MkNoteSub
v-if="conversation" v-if="conversation"
@ -17,38 +17,38 @@
:note="note" :note="note"
:detailedView="true" :detailedView="true"
/> />
<MkLoading v-else-if="note.reply" mini /> <MkLoading v-else-if="note.parent_note" mini />
<MkNoteSub <MkNoteSub
v-if="note.reply" v-if="note.parent_note"
:note="note.reply" :note="note.parent_note"
class="reply-to" class="reply-to"
:detailedView="true" :detailedView="true"
/> />
<MkNote <MagNote
ref="noteEl" ref="noteEl"
@contextmenu.stop="onContextmenu" @contextmenu.stop="onContextmenu"
tabindex="-1" tabindex="-1"
:note="note" :note="note"
detailedView detailedView
></MkNote> ></MagNote>
<MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab"> <MkTab v-model="tab" :style="'underline'" @update:modelValue="loadTab">
<option value="replies"> <option value="replies">
<!-- <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> --> <!-- <i class="ph-arrow-u-up-left ph-bold ph-lg"></i> -->
<span v-if="note.repliesCount > 0" class="count">{{ <span v-if="note.reply_count > 0" class="count">{{
note.repliesCount note.reply_count
}}</span> }}</span>
{{ i18n.ts._notification._types.reply }} {{ i18n.ts._notification._types.reply }}
</option> </option>
<option value="renotes" v-if="note.renoteCount > 0"> <option value="renotes" v-if="note.renote_count > 0">
<!-- <i class="ph-repeat ph-bold ph-lg"></i> --> <!-- <i class="ph-repeat ph-bold ph-lg"></i> -->
<span class="count">{{ note.renoteCount }}</span> <span class="count">{{ note.renote_count }}</span>
{{ i18n.ts._notification._types.renote }} {{ i18n.ts._notification._types.renote }}
</option> </option>
<option value="reactions" v-if="reactionsCount > 0"> <option value="reactions" v-if="magReactionCount(note) > 0">
<!-- <i class="ph-smiley ph-bold ph-lg"></i> --> <!-- <i class="ph-smiley ph-bold ph-lg"></i> -->
<span class="count">{{ reactionsCount }}</span> <span class="count">{{ magReactionCount(note) }}</span>
{{ i18n.ts.reaction }} {{ i18n.ts.reaction }}
</option> </option>
<option value="quotes" v-if="directQuotes?.length > 0"> <option value="quotes" v-if="directQuotes?.length > 0">
@ -73,7 +73,7 @@
:detailedView="true" :detailedView="true"
:parentId="note.id" :parentId="note.id"
/> />
<MkLoading v-else-if="tab === 'replies' && note.repliesCount > 0" /> <MkLoading v-else-if="tab === 'replies' && note.reply_count > 0" />
<MkNoteSub <MkNoteSub
v-if="directQuotes && tab === 'quotes'" v-if="directQuotes && tab === 'quotes'"
@ -101,7 +101,7 @@
:with-chart="false" :with-chart="false"
/> />
<!-- </MkPagination> --> <!-- </MkPagination> -->
<MkLoading v-else-if="tab === 'renotes' && note.renoteCount > 0" /> <MkLoading v-else-if="tab === 'renotes' && note.renote_count > 0" />
<div v-if="tab === 'clips' && clips.length > 0" class="_content clips"> <div v-if="tab === 'clips' && clips.length > 0" class="_content clips">
<MkA <MkA
@ -127,7 +127,7 @@
<MkLoading v-else-if="tab === 'clips' && clips.length > 0" /> <MkLoading v-else-if="tab === 'clips' && clips.length > 0" />
<MkReactedUsers <MkReactedUsers
v-if="tab === 'reactions' && reactionsCount > 0" v-if="tab === 'reactions' && magReactionCount(note) > 0"
:note-id="note.id" :note-id="note.id"
></MkReactedUsers> ></MkReactedUsers>
</div> </div>
@ -135,7 +135,7 @@
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small"> <I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
<template #name> <template #name>
<MkA <MkA
v-user-preview="note.userId" v-user-preview="note.user.id"
class="name" class="name"
:to="userPage(note.user)" :to="userPage(note.user)"
> >
@ -150,22 +150,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { import { onMounted, onUnmounted, onUpdated, ref, toRaw } from "vue";
computed,
inject,
onMounted,
onUnmounted,
onUpdated,
reactive,
ref,
} from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import MkTab from "@/components/MkTab.vue"; import MkTab from "@/components/MkTab.vue";
import MkNote from "@/components/MkNote.vue";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import XStarButton from "@/components/MkStarButton.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MkRenoteButton.vue";
import MkPagination from "@/components/MkPagination.vue";
import MkUserCardMini from "@/components/MkUserCardMini.vue"; import MkUserCardMini from "@/components/MkUserCardMini.vue";
import MkReactedUsers from "@/components/MkReactedUsers.vue"; import MkReactedUsers from "@/components/MkReactedUsers.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
@ -178,19 +167,20 @@ import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { getNoteMenu } from "@/scripts/get-note-menu"; import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture"; import { useNoteCapture } from "@/scripts/use-note-capture";
import { deepClone } from "@/scripts/clone";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { NoteUpdatedEvent } from "calckey-js/built/streaming.types"; import { NoteUpdatedEvent } from "calckey-js/built/streaming.types";
import appear from "@/directives/appear"; import { packed } from "magnetar-common";
import { magIsRenote, magReactionCount } from "@/scripts-mag/mag-util";
import MagNote from "@/components/MagNote.vue";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: packed.PackNoteMaybeFull;
pinned?: boolean; pinned?: boolean;
}>(); }>();
let tab = $ref("replies"); let tab = $ref("replies");
let note = $ref(deepClone(props.note)); let note = $ref<packed.PackNoteMaybeFull>(structuredClone(toRaw(props.note)));
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -205,7 +195,7 @@ const softMuteReasonI18nSrc = (what?: string) => {
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
onMounted(async () => { onMounted(async () => {
let result = deepClone(note); let result = structuredClone(toRaw(note));
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result); result = await interruptor.handler(result);
} }
@ -231,11 +221,6 @@ let clips = $ref();
let renotes = $ref(); let renotes = $ref();
let isScrolling; let isScrolling;
const reactionsCount = Object.values(props.note.reactions).reduce(
(x, y) => x + y,
0
);
const keymap = { const keymap = {
r: () => reply(true), r: () => reply(true),
"e|a|plus": () => react(true), "e|a|plus": () => react(true),
@ -294,7 +279,7 @@ function onContextmenu(ev: MouseEvent): void {
} }
}; };
if (isLink(ev.target)) return; if (isLink(ev.target)) return;
if (window.getSelection().toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
@ -344,7 +329,7 @@ os.api("notes/children", {
depth: 12, depth: 12,
}).then((res) => { }).then((res) => {
res = res.reduce((acc, resNote) => { res = res.reduce((acc, resNote) => {
if (resNote.userId == note.userId) { if (resNote.userId == note.user.id) {
return [...acc, resNote]; return [...acc, resNote];
} }
return [resNote, ...acc]; return [resNote, ...acc];
@ -357,9 +342,9 @@ os.api("notes/children", {
}); });
conversation = null; conversation = null;
if (note.replyId) { if (note.parent_note_id) {
os.api("notes/conversation", { os.api("notes/conversation", {
noteId: note.replyId, noteId: note.parent_note_id,
limit: 30, limit: 30,
}).then((res) => { }).then((res) => {
conversation = res.reverse(); conversation = res.reverse();

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="defgtij"> <div class="defgtij">
<div v-for="user in users" :key="user.id" class="avatar-holder"> <div v-for="user in users" :key="user.id" class="avatar-holder">
<MkAvatar :user="user" :show-indicator="true" class="avatar" /> <MkAvatar :user="user" class="avatar" />
</div> </div>
</div> </div>
</template> </template>

View File

@ -18,10 +18,12 @@ import { length } from "stringz";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { concat } from "@/scripts/array"; import { concat } from "@/scripts/array";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
modelValue: boolean; modelValue: boolean;
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -31,15 +33,18 @@ const emit = defineEmits<{
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const label = computed(() => { const label = computed(() => {
const attachments = magTransProperty(props.note, "attachments", "files");
const renote = magTransProperty(props.note, "renoted_note", "renote");
return concat([ return concat([
props.note.text props.note.text
? [i18n.t("_cw.chars", { count: length(props.note.text) })] ? [i18n.t("_cw.chars", { count: length(props.note.text) })]
: [], : [],
props.note.files && props.note.files.length !== 0 attachments && attachments.length !== 0
? [i18n.t("_cw.files", { count: props.note.files.length })] ? [i18n.t("_cw.files", { count: attachments.length })]
: [], : [],
props.note.poll != null ? [i18n.ts.poll] : [], props.note.poll != null ? [i18n.ts.poll] : [],
props.note.renote != null ? [i18n.ts.quoteAttached] : [], renote != null ? [i18n.ts.quoteAttached] : [],
] as string[][]).join(", "); ] as string[][]).join(", ");
}); });

View File

@ -19,8 +19,8 @@
}" }"
:disabled="wait" :disabled="wait"
@click.stop="onClick" @click.stop="onClick"
:aria-label="`${state} ${user.name || user.username}`" :aria-label="`${state} ${magTransUsername(user)}`"
v-tooltip="full ? null : `${state} ${user.name || user.username}`" v-tooltip="full ? null : `${state} ${magTransUsername(user)}`"
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="isBlocking"> <template v-if="isBlocking">
@ -28,13 +28,19 @@
><i class="ph-prohibit ph-bold ph-lg"></i> ><i class="ph-prohibit ph-bold ph-lg"></i>
</template> </template>
<template <template
v-else-if="hasPendingFollowRequestFromYou && user.isLocked" v-else-if="
hasPendingFollowRequestFromYou &&
magTransProperty(user, 'is_locked', 'isLocked')
"
> >
<span>{{ (state = i18n.ts.followRequestPending) }}</span <span>{{ (state = i18n.ts.followRequestPending) }}</span
><i class="ph-hourglass-medium ph-bold ph-lg"></i> ><i class="ph-hourglass-medium ph-bold ph-lg"></i>
</template> </template>
<template <template
v-else-if="hasPendingFollowRequestFromYou && !user.isLocked" v-else-if="
hasPendingFollowRequestFromYou &&
!magTransProperty(user, 'is_locked', 'isLocked')
"
> >
<!-- つまりリモートフォローの場合 --> <!-- つまりリモートフォローの場合 -->
<span>{{ (state = i18n.ts.processing) }}</span <span>{{ (state = i18n.ts.processing) }}</span
@ -44,11 +50,21 @@
<span>{{ (state = i18n.ts.unfollow) }}</span <span>{{ (state = i18n.ts.unfollow) }}</span
><i class="ph-minus ph-bold ph-lg"></i> ><i class="ph-minus ph-bold ph-lg"></i>
</template> </template>
<template v-else-if="!isFollowing && user.isLocked"> <template
v-else-if="
!isFollowing &&
magTransProperty(user, 'is_locked', 'isLocked')
"
>
<span>{{ (state = i18n.ts.followRequest) }}</span <span>{{ (state = i18n.ts.followRequest) }}</span
><i class="ph-plus ph-bold ph-lg"></i> ><i class="ph-plus ph-bold ph-lg"></i>
</template> </template>
<template v-else-if="!isFollowing && !user.isLocked"> <template
v-else-if="
!isFollowing &&
!magTransProperty(user, 'is_locked', 'isLocked')
"
>
<span>{{ (state = i18n.ts.follow) }}</span <span>{{ (state = i18n.ts.follow) }}</span
><i class="ph-plus ph-bold ph-lg"></i> ><i class="ph-plus ph-bold ph-lg"></i>
</template> </template>
@ -69,13 +85,15 @@ import { i18n } from "@/i18n";
import { $i } from "@/account"; import { $i } from "@/account";
import { getUserMenu } from "@/scripts/get-user-menu"; import { getUserMenu } from "@/scripts/get-user-menu";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { packed } from "magnetar-common";
import { magTransProperty, magTransUsername } from "@/scripts-mag/mag-util";
const router = useRouter(); const router = useRouter();
const emit = defineEmits(["refresh"]); const emit = defineEmits(["refresh"]);
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
user: Misskey.entities.UserDetailed; user: packed.PackUserMaybeAll | Misskey.entities.User;
full?: boolean; full?: boolean;
large?: boolean; large?: boolean;
hideMenu?: boolean; hideMenu?: boolean;
@ -86,24 +104,38 @@ const props = withDefaults(
} }
); );
const isBlocking = computed(() => props.user.isBlocking); const isBlocking = computed(() =>
magTransProperty(props.user, "you_block", "isBlocking")
);
let state = $ref(i18n.ts.processing); let state = $ref(i18n.ts.processing);
let isFollowing = $ref(props.user.isFollowing); let isFollowing = $ref(
magTransProperty(props.user, "you_follow", "isFollowing")
);
let hasPendingFollowRequestFromYou = $ref( let hasPendingFollowRequestFromYou = $ref(
props.user.hasPendingFollowRequestFromYou magTransProperty(
props.user,
"you_request_follow",
"hasPendingFollowRequestFromYou"
)
); );
let wait = $ref(false); let wait = $ref(false);
const connection = stream.useChannel("main"); const connection = stream.useChannel("main");
if (props.user.isFollowing == null) { if (
typeof magTransProperty(props.user, "you_follow", "isFollowing") !==
"boolean"
) {
os.api("users/show", { os.api("users/show", {
userId: props.user.id, userId: props.user.id,
}).then(onFollowChange); }).then(onFollowChange);
} }
function onFollowChange(user: Misskey.entities.UserDetailed) { function onFollowChange(user: Misskey.entities.User) {
if (!("isFollowing" in user && "hasPendingFollowRequestFromYou" in user))
return;
if (user.id === props.user.id) { if (user.id === props.user.id) {
isFollowing = user.isFollowing; isFollowing = user.isFollowing;
hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou; hasPendingFollowRequestFromYou = user.hasPendingFollowRequestFromYou;
@ -124,7 +156,7 @@ async function onClick() {
await os.api("blocking/delete", { await os.api("blocking/delete", {
userId: props.user.id, userId: props.user.id,
}); });
if (props.user.isMuted) { if (magTransProperty(props.user, "mute", "isMuted")) {
await os.api("mute/delete", { await os.api("mute/delete", {
userId: props.user.id, userId: props.user.id,
}); });
@ -134,7 +166,7 @@ async function onClick() {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "warning", type: "warning",
text: i18n.t("unfollowConfirm", { text: i18n.t("unfollowConfirm", {
name: props.user.name || props.user.username, name: magTransUsername(props.user),
}), }),
}); });

View File

@ -1,7 +1,12 @@
<template> <template>
<div <div
class="hpaizdrt" class="hpaizdrt"
v-tooltip="capitalize(instance.softwareName)" v-tooltip="
capitalize(
magTransProperty(instance, 'software_name', 'softwareName') ??
'?'
)
"
ref="ticker" ref="ticker"
:style="bg" :style="bg"
> >
@ -14,44 +19,59 @@
import { instanceName } from "@/config"; import { instanceName } from "@/config";
import { instance as Instance } from "@/instance"; import { instance as Instance } from "@/instance";
import { getProxiedImageUrlNullable } from "@/scripts/media-proxy"; import { getProxiedImageUrlNullable } from "@/scripts/media-proxy";
import * as Misskey from "calckey-js";
import { types } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
instance?: { instance?: Misskey.entities.User["instance"] | types.InstanceTicker | null;
faviconUrl?: string;
name: string;
themeColor?: string;
softwareName?: string;
};
}>(); }>();
let ticker = $ref<HTMLElement | null>(null); let ticker = $ref<HTMLElement | null>(null);
// if no instance data is given, this is for the local instance // if no instance data is given, this is for the local instance
const instance = props.instance ?? { const instance = props.instance ?? {
faviconUrl: Instance.iconUrl || Instance.faviconUrl || "/favicon.ico", iconUrl: null,
faviconUrl: (Instance.iconUrl || Instance.faviconUrl || "/favicon.ico") as
| string
| null,
name: instanceName, name: instanceName,
themeColor: ( themeColor: (
document.querySelector( document.querySelector(
'meta[name="theme-color-orig"]' 'meta[name="theme-color-orig"]'
) as HTMLMetaElement ) as HTMLMetaElement
)?.content, )?.content,
softwareName: Instance.softwareName || "Calckey", softwareName: (Instance.softwareName || "Magnetar") as string | null,
softwareVersion: (Instance.softwareVersion || Instance.version || "") as
| string
| null,
}; };
const capitalize = (s: string) => s && s[0].toUpperCase() + s.slice(1); const capitalize = (s: string) => s && s[0].toUpperCase() + s.slice(1);
const computedStyle = getComputedStyle(document.documentElement); const computedStyle = getComputedStyle(document.documentElement);
const themeColor = const themeColor =
instance.themeColor ?? computedStyle.getPropertyValue("--bg"); magTransProperty(instance, "theme_color", "themeColor") ??
computedStyle.getPropertyValue("--bg");
const bg = { const bg = {
background: `linear-gradient(90deg, ${themeColor}, ${themeColor}55)`, background: `linear-gradient(90deg, ${themeColor}, ${themeColor}55)`,
}; };
function getInstanceIcon(instance): string { function getInstanceIcon(
instance?: Misskey.entities.User["instance"] | types.InstanceTicker | null
): string {
if (!instance) return "/client-assets/dummy.png";
return ( return (
getProxiedImageUrlNullable(instance.iconUrl, "preview") ?? getProxiedImageUrlNullable(
getProxiedImageUrlNullable(instance.faviconUrl, "preview") ?? magTransProperty(instance, "icon_url", "iconUrl"),
"preview"
) ??
getProxiedImageUrlNullable(
magTransProperty(instance, "favicon_url", "faviconUrl"),
"preview"
) ??
"/client-assets/dummy.png" "/client-assets/dummy.png"
); );
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="mk-media-banner" @click.stop> <div class="mk-media-banner" @click.stop>
<div <div
v-if="media.isSensitive && hide" v-if="magTransProperty(media, 'sensitive', 'isSensitive') && hide"
class="sensitive" class="sensitive"
@click="hide = false" @click="hide = false"
> >
@ -11,7 +11,9 @@
</div> </div>
<div <div
v-else-if=" v-else-if="
media.type.startsWith('audio') && media.type !== 'audio/midi' ((type) => type.startsWith('audio') && type !== 'audio/midi')(
magTransProperty(media, 'mime_type', 'type')
)
" "
class="audio" class="audio"
> >
@ -62,10 +64,12 @@ import type * as misskey from "calckey-js";
import { ColdDeviceStorage } from "@/store"; import { ColdDeviceStorage } from "@/store";
import "vue-plyr/dist/vue-plyr.css"; import "vue-plyr/dist/vue-plyr.css";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
media: misskey.entities.DriveFile; media: packed.PackDriveFileBase | misskey.entities.DriveFile;
}>(), }>(),
{} {}
); );

View File

@ -53,12 +53,19 @@
@click="$refs.modal.close()" @click="$refs.modal.close()"
/> />
<footer> <footer>
<span>{{ image.type }}</span> <span>{{ image.mime_type || image.type }}</span>
<span>{{ bytes(image.size) }}</span> <span>{{ bytes(image.size) }}</span>
<span v-if="image.properties && image.properties.width" <span v-if="image.properties && image.properties.width"
>{{ number(image.properties.width) }}px × >{{ number(image.properties.width) }}px ×
{{ number(image.properties.height) }}px</span {{ number(image.properties.height) }}px</span
> >
<span
v-if="
image.media_metadata && image.media_metadata.width
"
>{{ number(image.media_metadata.width) }}px ×
{{ number(image.media_metadata.height) }}px</span
>
</footer> </footer>
</div> </div>
</div> </div>

View File

@ -1,9 +1,9 @@
<template> <template>
<button v-if="hide" class="qjewsnkg" @click="hide = false"> <button v-if="hide" class="qjewsnkg" @click="hide = false">
<ImgWithBlurhash <ImgWithBlurhash
:hash="image.blurhash" :hash="image.blurhash ?? undefined"
:title="image.comment" :title="image.comment"
:alt="image.comment" :alt="image.comment ?? ''"
/> />
<div class="text"> <div class="text">
<div class="wrapper"> <div class="wrapper">
@ -18,14 +18,21 @@
<div v-else class="gqnyydlz"> <div v-else class="gqnyydlz">
<a :href="image.url" :title="image.name"> <a :href="image.url" :title="image.name">
<ImgWithBlurhash <ImgWithBlurhash
:hash="image.blurhash" :hash="image.blurhash ?? undefined"
:src="url" :src="url"
:alt="image.comment" :alt="image.comment ?? ''"
:type="image.type" :type="magTransProperty(image, 'mime_type', 'type')"
:title="image.comment" :title="image.comment"
:cover="false" :cover="false"
/> />
<div v-if="image.type === 'image/gif'" class="gif">GIF</div> <div
v-if="
magTransProperty(image, 'mime_type', 'type') === 'image/gif'
"
class="gif"
>
GIF
</div>
</a> </a>
<div class="_image_controls"> <div class="_image_controls">
<button <button
@ -63,9 +70,11 @@ import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue"; import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
image: misskey.entities.DriveFile; image: packed.PackDriveFileBase | misskey.entities.DriveFile;
raw?: boolean; raw?: boolean;
}>(); }>();
@ -95,8 +104,10 @@ const url =
props.raw || defaultStore.state.loadRawImages props.raw || defaultStore.state.loadRawImages
? props.image.url ? props.image.url
: defaultStore.state.disableShowingAnimatedImages : defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.image.thumbnailUrl) ? getStaticImageUrl(
: props.image.thumbnailUrl; magTransProperty(props.image, "thumbnail_url", "thumbnailUrl")!
)
: magTransProperty(props.image, "thumbnail_url", "thumbnailUrl")!;
// Plugin:register_note_view_interruptor 使watch // Plugin:register_note_view_interruptor 使watch
watch( watch(
@ -105,7 +116,7 @@ watch(
hide = hide =
defaultStore.state.nsfw === "force" defaultStore.state.nsfw === "force"
? true ? true
: props.image.isSensitive && : magTransProperty(props.image, "sensitive", "isSensitive") &&
defaultStore.state.nsfw !== "ignore"; defaultStore.state.nsfw !== "ignore";
}, },
{ {

View File

@ -18,12 +18,24 @@
)" )"
> >
<XVideo <XVideo
v-if="media.type.startsWith('video')" v-if="
magTransProperty(
media,
'mime_type',
'type'
).startsWith('video')
"
:key="media.id" :key="media.id"
:video="media" :video="media"
/> />
<XImage <XImage
v-else-if="media.type.startsWith('image')" v-else-if="
magTransProperty(
media,
'mime_type',
'type'
).startsWith('image')
"
:key="media.id" :key="media.id"
class="image" class="image"
:data-id="media.id" :data-id="media.id"
@ -47,10 +59,11 @@ import XImage from "@/components/MkMediaImage.vue";
import XVideo from "@/components/MkMediaVideo.vue"; import XVideo from "@/components/MkMediaVideo.vue";
import * as os from "@/os"; import * as os from "@/os";
import { FILE_TYPE_BROWSERSAFE } from "@/const"; import { FILE_TYPE_BROWSERSAFE } from "@/const";
import { defaultStore } from "@/store"; import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
mediaList: misskey.entities.DriveFile[]; mediaList: (packed.PackDriveFileBase | misskey.entities.DriveFile)[];
raw?: boolean; raw?: boolean;
inDm?: boolean; inDm?: boolean;
}>(); }>();
@ -62,22 +75,30 @@ onMounted(() => {
const lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList dataSource: props.mediaList
.filter((media) => { .filter((media) => {
if (media.type === "image/svg+xml") return true; // svgwebpublicpngtrue const type = magTransProperty(media, "mime_type", "type");
if (type === "image/svg+xml") return true; // svgwebpublicpngtrue
return ( return (
media.type.startsWith("image") && type.startsWith("image") &&
FILE_TYPE_BROWSERSAFE.includes(media.type) FILE_TYPE_BROWSERSAFE.includes(type)
); );
}) })
.map((media) => { .map((media) => {
const properties = magTransProperty(
media,
"media_metadata",
"properties"
);
const item = { const item = {
src: media.url, src: media.url,
w: media.properties.width, w: properties.width,
h: media.properties.height, h: properties.height,
alt: media.comment, alt: media.comment,
}; };
if ( if (
media.properties.orientation != null && properties.orientation != null &&
media.properties.orientation >= 5 properties.orientation >= 5
) { ) {
[item.w, item.h] = [item.h, item.w]; [item.w, item.h] = [item.h, item.w];
} }
@ -116,16 +137,21 @@ onMounted(() => {
const id = element.dataset.id; const id = element.dataset.id;
const file = props.mediaList.find((media) => media.id === id); const file = props.mediaList.find((media) => media.id === id);
if (!file) return;
const properties = magTransProperty(
file,
"media_metadata",
"properties"
);
itemData.src = file.url; itemData.src = file.url;
itemData.w = Number(file.properties.width); itemData.w = Number(properties.width);
itemData.h = Number(file.properties.height); itemData.h = Number(properties.height);
if ( if (properties.orientation != null && properties.orientation >= 5) {
file.properties.orientation != null &&
file.properties.orientation >= 5
) {
[itemData.w, itemData.h] = [itemData.h, itemData.w]; [itemData.w, itemData.h] = [itemData.h, itemData.w];
} }
itemData.msrc = file.thumbnailUrl; itemData.msrc = magTransProperty(file, "thumbnail_url", "thumbnailUrl");
itemData.alt = file.comment; itemData.alt = file.comment;
itemData.thumbCropped = true; itemData.thumbCropped = true;
}); });
@ -178,12 +204,16 @@ onMounted(() => {
} }
}); });
const previewable = (file: misskey.entities.DriveFile): boolean => { const previewable = (
if (file.type === "image/svg+xml") return true; // svgwebpublic/thumbnailpngtrue file: packed.PackDriveFileBase | misskey.entities.DriveFile
): boolean => {
const type = magTransProperty(file, "mime_type", "type");
if (type === "image/svg+xml") return true; // svgwebpublic/thumbnailpngtrue
// FILE_TYPE_BROWSERSAFE // FILE_TYPE_BROWSERSAFE
return ( return (
(file.type.startsWith("video") || file.type.startsWith("image")) && (type.startsWith("video") || type.startsWith("image")) &&
FILE_TYPE_BROWSERSAFE.includes(file.type) FILE_TYPE_BROWSERSAFE.includes(type)
); );
}; };
const previewableCount = props.mediaList.filter((media) => const previewableCount = props.mediaList.filter((media) =>

View File

@ -31,14 +31,19 @@
}" }"
> >
<video <video
:poster="video.thumbnailUrl" :poster="
magTransProperty(video, 'thumbnail_url', 'thumbnailUrl')
"
:title="video.comment" :title="video.comment"
:aria-label="video.comment" :aria-label="video.comment"
preload="none" preload="none"
controls controls
@contextmenu.stop @contextmenu.stop
> >
<source :src="video.url" :type="video.type" /> <source
:src="video.url"
:type="magTransProperty(video, 'mime_type', 'type')"
/>
</video> </video>
</VuePlyr> </VuePlyr>
<button <button
@ -58,9 +63,11 @@ import type * as misskey from "calckey-js";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import "vue-plyr/dist/vue-plyr.css"; import "vue-plyr/dist/vue-plyr.css";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
video: misskey.entities.DriveFile; video: packed.PackDriveFileBase | misskey.entities.DriveFile;
}>(); }>();
const plyr = ref(); const plyr = ref();
@ -69,7 +76,8 @@ const mini = ref(false);
const hide = ref( const hide = ref(
defaultStore.state.nsfw === "force" defaultStore.state.nsfw === "force"
? true ? true
: props.video.isSensitive && defaultStore.state.nsfw !== "ignore" : magTransProperty(props.video, "sensitive", "isSensitive") &&
defaultStore.state.nsfw !== "ignore"
); );
onMounted(() => { onMounted(() => {

View File

@ -235,28 +235,23 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, reactive, ref } from "vue";
import * as mfm from "mfm-js";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { computed, inject, onMounted, ref, toRaw } from "vue";
import * as mfm from "mfm-js";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import MkNoteSub from "@/components/MkNoteSub.vue"; import MkNoteSub from "@/components/MkNoteSub.vue";
import MkSubNoteContent from "./MkSubNoteContent.vue"; import MkSubNoteContent from "./MkSubNoteContent.vue";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import XNoteSimple from "@/components/MkNoteSimple.vue";
import XMediaList from "@/components/MkMediaList.vue";
import XCwButton from "@/components/MkCwButton.vue";
import XPoll from "@/components/MkPoll.vue";
import XRenoteButton from "@/components/MkRenoteButton.vue"; import XRenoteButton from "@/components/MkRenoteButton.vue";
import XReactionsViewer from "@/components/MkReactionsViewer.vue"; import XReactionsViewer from "@/components/MkReactionsViewer.vue";
import XStarButton from "@/components/MkStarButton.vue"; import XStarButton from "@/components/MkStarButton.vue";
import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue"; import XStarButtonNoEmoji from "@/components/MkStarButtonNoEmoji.vue";
import XQuoteButton from "@/components/MkQuoteButton.vue"; import XQuoteButton from "@/components/MkQuoteButton.vue";
import MkUrlPreview from "@/components/MkUrlPreview.vue";
import MkVisibility from "@/components/MkVisibility.vue"; import MkVisibility from "@/components/MkVisibility.vue";
import copyToClipboard from "@/scripts/copy-to-clipboard"; import copyToClipboard from "@/scripts/copy-to-clipboard";
import { url } from "@/config"; import { url } from "@/config";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import { focusPrev, focusNext } from "@/scripts/focus"; import { focusNext, focusPrev } from "@/scripts/focus";
import { getWordSoftMute } from "@/scripts/check-word-mute"; import { getWordSoftMute } from "@/scripts/check-word-mute";
import { useRouter } from "@/router"; import { useRouter } from "@/router";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
@ -268,7 +263,6 @@ import { i18n } from "@/i18n";
import { getNoteMenu } from "@/scripts/get-note-menu"; import { getNoteMenu } from "@/scripts/get-note-menu";
import { useNoteCapture } from "@/scripts/use-note-capture"; import { useNoteCapture } from "@/scripts/use-note-capture";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { deepClone } from "@/scripts/clone";
import { getNoteSummary } from "@/scripts/get-note-summary"; import { getNoteSummary } from "@/scripts/get-note-summary";
const router = useRouter(); const router = useRouter();
@ -280,7 +274,7 @@ const props = defineProps<{
collapsedReply?: boolean; collapsedReply?: boolean;
}>(); }>();
let note = $ref(deepClone(props.note)); let note = $ref<misskey.entities.Note>(structuredClone(toRaw(props.note)));
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -295,7 +289,7 @@ const softMuteReasonI18nSrc = (what?: string) => {
// plugin // plugin
if (noteViewInterruptors.length > 0) { if (noteViewInterruptors.length > 0) {
onMounted(async () => { onMounted(async () => {
let result = deepClone(note); let result = structuredClone(toRaw(note));
for (const interruptor of noteViewInterruptors) { for (const interruptor of noteViewInterruptors) {
result = await interruptor.handler(result); result = await interruptor.handler(result);
} }

View File

@ -9,7 +9,13 @@
@click.stop @click.stop
> >
<MkUserName :user="note.user" class="mkusername"> <MkUserName :user="note.user" class="mkusername">
<span v-if="note.user.isBot" class="is-bot">bot</span> <span
v-if="
magTransProperty(note.user, 'is_bot', 'isBot')
"
class="is-bot"
>bot</span
>
</MkUserName> </MkUserName>
</MkA> </MkA>
<div class="username"><MkAcct :user="note.user" /></div> <div class="username"><MkAcct :user="note.user" /></div>
@ -17,16 +23,38 @@
<div> <div>
<div class="info"> <div class="info">
<MkA class="created-at" :to="notePage(note)"> <MkA class="created-at" :to="notePage(note)">
<MkTime :time="note.createdAt" /> <MkTime
:time="
magTransProperty(
note,
'created_at',
'createdAt'
)
"
/>
<i <i
v-if="note.updatedAt" v-if="
magTransProperty(
note,
'updated_at',
'updatedAt'
)
"
v-tooltip.noDelay=" v-tooltip.noDelay="
i18n.t('edited', { i18n.t('edited', {
date: new Date( date: new Date(
note.updatedAt magTransProperty(
note,
'updated_at',
'updatedAt'
)!
).toLocaleDateString(), ).toLocaleDateString(),
time: new Date( time: new Date(
note.updatedAt magTransProperty(
note,
'updated_at',
'updatedAt'
)!
).toLocaleTimeString(), ).toLocaleTimeString(),
}) })
" "
@ -37,7 +65,7 @@
<MkVisibility :note="note" /> <MkVisibility :note="note" />
</div> </div>
<MkInstanceTicker <MkInstanceTicker
v-if="showTicker" v-if="showTicker && note.user.instance"
class="ticker" class="ticker"
:instance="note.user.instance" :instance="note.user.instance"
/> />
@ -47,17 +75,18 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue";
import type * as misskey from "calckey-js"; import type * as misskey from "calckey-js";
import { defaultStore, noteViewInterruptors } from "@/store"; import { defaultStore } from "@/store";
import MkVisibility from "@/components/MkVisibility.vue"; import MkVisibility from "@/components/MkVisibility.vue";
import MkInstanceTicker from "@/components/MkInstanceTicker.vue"; import MkInstanceTicker from "@/components/MkInstanceTicker.vue";
import { notePage } from "@/filters/note"; import { notePage } from "@/filters/note";
import { userPage } from "@/filters/user"; import { userPage } from "@/filters/user";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: packed.PackNoteBase | misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
}>(); }>();
@ -65,7 +94,7 @@ let note = $ref(props.note);
const showTicker = const showTicker =
defaultStore.state.instanceTicker === "always" || defaultStore.state.instanceTicker === "always" ||
(defaultStore.state.instanceTicker === "remote" && note.user.instance); (defaultStore.state.instanceTicker === "remote" && note.user.host);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -11,13 +11,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue"; import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
pinned?: boolean; pinned?: boolean;
}>(); }>();
</script> </script>

View File

@ -68,32 +68,52 @@
@click="reply()" @click="reply()"
> >
<i class="ph-arrow-u-up-left ph-bold ph-lg"></i> <i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
<template v-if="appearNote.repliesCount > 0"> <template
<p class="count">{{ appearNote.repliesCount }}</p> v-if="
Number(
magTransProperty(
appearNote,
'reply_count',
'repliesCount'
)
) > 0
"
>
<p class="count">
{{
magTransProperty(
appearNote,
"reply_count",
"repliesCount"
)
}}
</p>
</template> </template>
</button> </button>
<XRenoteButton <XRenoteButton
ref="renoteButton" ref="renoteButton"
class="button" class="button"
:note="appearNote" :note="appearNote"
:count="appearNote.renoteCount" :count="
Number(
magTransProperty(
appearNote,
'renote_count',
'renoteCount'
)
)
"
/> />
<XStarButtonNoEmoji <XStarButtonNoEmoji
v-if="!enableEmojiReactions" v-if="!enableEmojiReactions"
class="button" class="button"
:note="appearNote" :note="appearNote"
:count=" :count="magReactionCount(appearNote)"
Object.values(appearNote.reactions).reduce( :reacted="magHasReacted(appearNote)"
(partialSum, val) => partialSum + val,
0
)
"
:reacted="appearNote.myReaction != null"
/> />
<XStarButton <XStarButton
v-if=" v-if="
enableEmojiReactions && enableEmojiReactions && !magHasReacted(appearNote)
appearNote.myReaction == null
" "
ref="starButton" ref="starButton"
class="button" class="button"
@ -101,8 +121,7 @@
/> />
<button <button
v-if=" v-if="
enableEmojiReactions && enableEmojiReactions && !magHasReacted(appearNote)
appearNote.myReaction == null
" "
ref="reactButton" ref="reactButton"
v-tooltip.noDelay.bottom="i18n.ts.reaction" v-tooltip.noDelay.bottom="i18n.ts.reaction"
@ -112,10 +131,7 @@
<i class="ph-smiley ph-bold ph-lg"></i> <i class="ph-smiley ph-bold ph-lg"></i>
</button> </button>
<button <button
v-if=" v-if="enableEmojiReactions && magHasReacted(appearNote)"
enableEmojiReactions &&
appearNote.myReaction != null
"
ref="reactButton" ref="reactButton"
class="button _button reacted" class="button _button reacted"
@click="undoReact(appearNote)" @click="undoReact(appearNote)"
@ -162,7 +178,9 @@
<I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small"> <I18n :src="softMuteReasonI18nSrc(muted.what)" tag="small">
<template #name> <template #name>
<MkA <MkA
v-user-preview="note.userId" v-user-preview="
magTransMap(note, 'user', 'userId', (u) => u.id)
"
class="name" class="name"
:to="userPage(note.user)" :to="userPage(note.user)"
> >
@ -177,8 +195,8 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { inject, ref } from "vue";
import type { Ref } from "vue"; import type { Ref } from "vue";
import { inject, ref, toRaw } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import XNoteHeader from "@/components/MkNoteHeader.vue"; import XNoteHeader from "@/components/MkNoteHeader.vue";
import MkSubNoteContent from "@/components/MkSubNoteContent.vue"; import MkSubNoteContent from "@/components/MkSubNoteContent.vue";
@ -201,13 +219,20 @@ import { $i } from "@/account";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { useNoteCapture } from "@/scripts/use-note-capture"; import { useNoteCapture } from "@/scripts/use-note-capture";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { deepClone } from "@/scripts/clone"; import { packed } from "magnetar-common";
import {
magHasReacted,
magIsRenote,
magReactionCount,
magTransMap,
magTransProperty,
} from "@/scripts-mag/mag-util";
const router = useRouter(); const router = useRouter();
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
conversation?: misskey.entities.Note[]; conversation?: misskey.entities.Note[];
parentId?; parentId?;
detailedView?; detailedView?;
@ -223,7 +248,9 @@ const props = withDefaults(
} }
); );
let note = $ref(deepClone(props.note)); let note = $ref<packed.PackNoteMaybeFull | misskey.entities.Note>(
structuredClone(toRaw(props.note))
);
const softMuteReasonI18nSrc = (what?: string) => { const softMuteReasonI18nSrc = (what?: string) => {
if (what === "note") return i18n.ts.userSaysSomethingReason; if (what === "note") return i18n.ts.userSaysSomethingReason;
@ -235,12 +262,6 @@ const softMuteReasonI18nSrc = (what?: string) => {
return i18n.ts.userSaysSomething; return i18n.ts.userSaysSomething;
}; };
const isRenote =
note.renote != null &&
note.text == null &&
note.fileIds.length === 0 &&
note.poll == null;
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const footerEl = ref<HTMLElement>(); const footerEl = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
@ -248,7 +269,11 @@ const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement>();
let appearNote = $computed(() => let appearNote = $computed(() =>
isRenote ? (note.renote as misskey.entities.Note) : note magIsRenote(note)
? (magTransProperty(note, "renoted_note", "renote") as
| packed.PackNoteMaybeFull
| misskey.entities.Note)
: note
); );
const isDeleted = ref(false); const isDeleted = ref(false);
const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords)); const muted = ref(getWordSoftMute(note, $i, defaultStore.state.mutedWords));
@ -329,6 +354,8 @@ function menu(viaKeyboard = false): void {
} }
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
if (!ev.target || !(ev.target instanceof HTMLElement)) return;
const isLink = (el: HTMLElement) => { const isLink = (el: HTMLElement) => {
if (el.tagName === "A") return true; if (el.tagName === "A") return true;
if (el.parentElement) { if (el.parentElement) {
@ -336,7 +363,7 @@ function onContextmenu(ev: MouseEvent): void {
} }
}; };
if (isLink(ev.target)) return; if (isLink(ev.target)) return;
if (window.getSelection().toString() !== "") return; if (window.getSelection()?.toString() !== "") return;
if (defaultStore.state.useReactionPickerForContextMenu) { if (defaultStore.state.useReactionPickerForContextMenu) {
ev.preventDefault(); ev.preventDefault();
@ -395,15 +422,15 @@ function onContextmenu(ev: MouseEvent): void {
} }
function focus() { function focus() {
el.value.focus(); el.value?.focus();
} }
function blur() { function blur() {
el.value.blur(); el.value?.blur();
} }
function noteClick(e) { function noteClick(e) {
if (document.getSelection().type === "Range" || !expandOnNoteClick) { if (document.getSelection()?.type === "Range" || !expandOnNoteClick) {
e.stopPropagation(); e.stopPropagation();
} else { } else {
router.push(notePage(props.note)); router.push(notePage(props.note));

View File

@ -2,31 +2,50 @@
<div class="tivcixzd" :class="{ done: closed || isVoted }"> <div class="tivcixzd" :class="{ done: closed || isVoted }">
<ul> <ul>
<li <li
v-for="(choice, i) in note.poll.choices" v-for="(choice, i) in magTransProperty(
note.poll,
'options',
'choices'
)"
:key="i" :key="i"
:class="{ voted: choice.voted }" :class="{ voted: magTransProperty(choice, 'voted', 'isVoted') }"
@click.stop="vote(i)" @click.stop="vote(i)"
> >
<div <div
class="backdrop" class="backdrop"
:style="{ :style="{
width: `${ width: `${
showResult ? (choice.votes / total) * 100 : 0 showResult
? (magTransProperty(
choice,
'votes_count',
'votes'
) /
total) *
100
: 0
}%`, }%`,
}" }"
></div> ></div>
<span> <span>
<template v-if="choice.isVoted" <template
v-if="magTransProperty(choice, 'voted', 'isVoted')"
><i class="ph-check ph-bold ph-lg"></i ><i class="ph-check ph-bold ph-lg"></i
></template> ></template>
<Mfm <Mfm
:text="choice.text" :text="magTransProperty(choice, 'title', 'text')"
:plain="true" :plain="true"
:custom-emojis="note.emojis" :custom-emojis="note.emojis"
/> />
<span v-if="showResult" class="votes" <span v-if="showResult" class="votes"
>({{ >({{
i18n.t("_poll.votesCount", { n: choice.votes }) i18n.t("_poll.votesCount", {
n: magTransProperty(
choice,
"votes_count",
"votes"
),
})
}})</span }})</span
> >
</span> </span>
@ -65,22 +84,35 @@ import { pleaseLogin } from "@/scripts/please-login";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { useInterval } from "@/scripts/use-interval"; import { useInterval } from "@/scripts/use-interval";
import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note & Required<Pick<misskey.entities.Note, "poll">>; note:
| (packed.PackNoteMaybeFull & { poll: {} })
| (misskey.entities.Note &
Required<Pick<misskey.entities.Note, "poll">>);
readOnly?: boolean; readOnly?: boolean;
}>(); }>();
const pollRefreshing = ref(false); const pollRefreshing = ref(false);
const remaining = ref(-1); const remaining = ref(-1);
const total = computed(() => sum(props.note.poll.choices.map((x) => x.votes))); const total = computed(() =>
sum(
magTransProperty(props.note.poll, "options", "choices").map(
(x) => magTransProperty(x, "votes_count", "votes") as number
)
)
);
const closed = computed(() => remaining.value === 0); const closed = computed(() => remaining.value === 0);
const isLocal = computed(() => !props.note.uri); const isLocal = computed(() => !props.note.uri);
const isVoted = computed( const isVoted = computed(
() => () =>
!props.note.poll.multiple && !magTransProperty(props.note.poll, "multiple_choice", "multiple") &&
props.note.poll.choices.some((c) => c.isVoted) magTransProperty(props.note.poll, "options", "choices").some(
(c) => magTransProperty(c, "voted", "isVoted") ?? false
)
); );
const timer = computed(() => const timer = computed(() =>
i18n.t( i18n.t(
@ -103,11 +135,17 @@ const timer = computed(() =>
const showResult = ref(props.readOnly || isVoted.value); const showResult = ref(props.readOnly || isVoted.value);
// //
if (props.note.poll.expiresAt) { if (magTransProperty(props.note.poll, "expires_at", "expiresAt")) {
const tick = () => { const tick = () => {
remaining.value = Math.floor( remaining.value = Math.floor(
Math.max( Math.max(
new Date(props.note.poll.expiresAt!).getTime() - Date.now(), new Date(
magTransProperty(
props.note.poll,
"expires_at",
"expiresAt"
)!
).getTime() - Date.now(),
0 0
) / 1000 ) / 1000
); );
@ -152,7 +190,11 @@ const vote = async (id: number) => {
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: "question", type: "question",
text: i18n.t("voteConfirm", { text: i18n.t("voteConfirm", {
choice: props.note.poll.choices[id].text, choice: magTransProperty(
magTransProperty(props.note.poll, "options", "choices")[id],
"title",
"text"
),
}), }),
}); });
if (canceled) return; if (canceled) return;
@ -161,7 +203,12 @@ const vote = async (id: number) => {
noteId: props.note.id, noteId: props.note.id,
choice: id, choice: id,
}); });
if (!showResult.value) showResult.value = !props.note.poll.multiple; if (!showResult.value)
showResult.value = !magTransProperty(
props.note.poll,
"multiple_choice",
"multiple"
);
}; };
</script> </script>

View File

@ -252,24 +252,32 @@ import {
openAccountMenu as openAccountMenu_, openAccountMenu as openAccountMenu_,
} from "@/account"; } from "@/account";
import { uploadFile } from "@/scripts/upload"; import { uploadFile } from "@/scripts/upload";
import { deepClone } from "@/scripts/clone";
import XCheatSheet from "@/components/MkCheatSheetDialog.vue"; import XCheatSheet from "@/components/MkCheatSheetDialog.vue";
import { preprocess } from "@/scripts/preprocess"; import { preprocess } from "@/scripts/preprocess";
import { packed } from "magnetar-common";
import {
magLegacyVisibility,
magTransMap,
magTransProperty,
} from "@/scripts-mag/mag-util";
const modal = inject("modal"); const modal = inject("modal");
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
reply?: misskey.entities.Note; reply?: packed.PackNoteMaybeFull | misskey.entities.Note;
renote?: misskey.entities.Note; renote?: packed.PackNoteMaybeFull | misskey.entities.Note;
mention?: misskey.entities.User; mention?: misskey.entities.User;
specified?: misskey.entities.User; specified?: misskey.entities.User;
initialText?: string; initialText?: string;
initialVisibility?: typeof misskey.noteVisibilities; initialVisibility?: typeof misskey.noteVisibilities;
initialFiles?: misskey.entities.DriveFile[]; initialFiles?: (
| packed.PackDriveFileBase
| misskey.entities.DriveFile
)[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: misskey.entities.User[]; initialVisibleUsers?: misskey.entities.User[];
initialNote?: misskey.entities.Note; initialNote?: packed.PackNoteMaybeFull | misskey.entities.Note;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;
@ -465,21 +473,26 @@ if (
) { ) {
visibility = "specified"; visibility = "specified";
} else { } else {
visibility = props.reply.visibility; visibility = magLegacyVisibility(props.reply.visibility);
} }
if (visibility === "specified") { if (visibility === "specified") {
if (props.reply.visibleUserIds) { const ids = magTransProperty(
props.reply,
"visible_user_ids",
"visibleUserIds"
);
if (ids) {
os.api("users/show", { os.api("users/show", {
userIds: props.reply.visibleUserIds.filter( userIds: ids.filter(
(uid) => uid !== $i.id && uid !== props.reply.userId (uid) => uid !== $i.id && uid !== props.reply!.user.id
), ),
}).then((users) => { }).then((users) => {
users.forEach(pushVisibleUser); users.forEach(pushVisibleUser);
}); });
} }
if (props.reply.userId !== $i.id) { if (props.reply.user.id !== $i.id) {
os.api("users/show", { userId: props.reply.userId }).then( os.api("users/show", { userId: props.reply.user.id }).then(
(user) => { (user) => {
pushVisibleUser(user); pushVisibleUser(user);
} }
@ -591,7 +604,10 @@ function updateFiles(_files) {
} }
function updateFileSensitive(file, sensitive) { function updateFileSensitive(file, sensitive) {
files[files.findIndex((x) => x.id === file.id)].isSensitive = sensitive; const idx = files.findIndex((x) => x.id === file.id);
const f = files[idx];
if ("isSensitive" in f) f.isSensitive = sensitive;
else f.sensitive = sensitive;
} }
function updateFileName(file, name) { function updateFileName(file, name) {
@ -830,7 +846,7 @@ async function post() {
// plugin // plugin
if (notePostInterruptors.length > 0) { if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) { for (const interruptor of notePostInterruptors) {
postData = await interruptor.handler(deepClone(postData)); postData = await interruptor.handler(structuredClone(postData));
} }
} }
@ -972,20 +988,39 @@ onMounted(() => {
if (props.initialNote) { if (props.initialNote) {
const init = props.initialNote; const init = props.initialNote;
text = init.text ? init.text : ""; text = init.text ? init.text : "";
files = init.files;
files = magTransProperty(init, "attachments", "files") ?? [];
cw = init.cw; cw = init.cw;
useCw = init.cw != null; useCw = init.cw != null;
if (init.poll) { if (init.poll) {
poll = { poll = {
choices: init.poll.choices.map((x) => x.text), choices: magTransMap(
multiple: init.poll.multiple, init.poll,
expiresAt: init.poll.expiresAt, "options",
expiredAfter: init.poll.expiredAfter, "choices",
(a) => a.map((x) => x.title),
(b) => b.map((x) => x.text)
),
multiple: magTransProperty(
init.poll,
"multiple_choice",
"multiple"
),
expiresAt: magTransProperty(
init.poll,
"expires_at",
"expiresAt"
),
// TODO(Natty)
expiredAfter: null,
}; };
} }
visibility = init.visibility; visibility = magLegacyVisibility(init.visibility);
localOnly = init.localOnly; localOnly =
quoteId = init.renote ? init.renote.id : null; magTransProperty(init, "local_only", "localOnly") ?? false;
quoteId = magTransProperty(init, "renote", "renoted_note")
? magTransProperty(init, "renote", "renoted_note")!.id
: null;
} }
nextTick(() => watchForDraft()); nextTick(() => watchForDraft());

View File

@ -20,7 +20,10 @@
:file="element" :file="element"
fit="cover" fit="cover"
/> />
<div v-if="element.isSensitive" class="sensitive"> <div
v-if="element.sensitive ?? element.isSensitive"
class="sensitive"
>
<i class="ph-warning ph-bold ph-lg icon"></i> <i class="ph-warning ph-bold ph-lg icon"></i>
</div> </div>
</div> </div>
@ -31,7 +34,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, defineAsyncComponent } from "vue"; import { defineAsyncComponent, defineComponent } from "vue";
import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue"; import MkDriveFileThumbnail from "@/components/MkDriveFileThumbnail.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
@ -86,9 +89,13 @@ export default defineComponent({
toggleSensitive(file) { toggleSensitive(file) {
os.api("drive/files/update", { os.api("drive/files/update", {
fileId: file.id, fileId: file.id,
isSensitive: !file.isSensitive, isSensitive: !(file.sensitive ?? file.isSensitive),
}).then(() => { }).then(() => {
this.$emit("changeSensitive", file, !file.isSensitive); this.$emit(
"changeSensitive",
file,
!(file.sensitive ?? file.isSensitive)
);
}); });
}, },
async rename(file) { async rename(file) {
@ -150,9 +157,10 @@ export default defineComponent({
}, },
}, },
{ {
text: file.isSensitive text:
? i18n.ts.unmarkAsSensitive file.sensitive ?? file.isSensitive
: i18n.ts.markAsSensitive, ? i18n.ts.unmarkAsSensitive
: i18n.ts.markAsSensitive,
icon: file.isSensitive icon: file.isSensitive
? "ph-eye ph-bold ph-lg" ? "ph-eye ph-bold ph-lg"
: "ph-eye-slash ph-bold ph-lg", : "ph-eye-slash ph-bold ph-lg",

View File

@ -19,14 +19,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import MkModal from "@/components/MkModal.vue"; import MkModal from "@/components/MkModal.vue";
import MkPostForm from "@/components/MkPostForm.vue"; import MkPostForm from "@/components/MkPostForm.vue";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
reply?: misskey.entities.Note; reply?: packed.PackNoteMaybeFull | misskey.entities.Note;
renote?: misskey.entities.Note; renote?: packed.PackNoteMaybeFull | misskey.entities.Note;
mention?: misskey.entities.User; mention?: misskey.entities.User;
specified?: misskey.entities.User; specified?: misskey.entities.User;
initialText?: string; initialText?: string;
@ -34,7 +34,7 @@ const props = defineProps<{
initialFiles?: misskey.entities.DriveFile[]; initialFiles?: misskey.entities.DriveFile[];
initialLocalOnly?: boolean; initialLocalOnly?: boolean;
initialVisibleUsers?: misskey.entities.User[]; initialVisibleUsers?: misskey.entities.User[];
initialNote?: misskey.entities.Note; initialNote?: packed.PackNoteMaybeFull | misskey.entities.Note;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;

View File

@ -5,7 +5,7 @@
v-ripple="canToggle" v-ripple="canToggle"
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ :class="{
reacted: note.myReaction == reaction, reacted: magReactionSelf(note) === reaction,
canToggle, canToggle,
newlyAdded: !isInitial, newlyAdded: !isInitial,
}" }"
@ -28,12 +28,14 @@ import XReactionIcon from "@/components/MkReactionIcon.vue";
import * as os from "@/os"; import * as os from "@/os";
import { useTooltip } from "@/scripts/use-tooltip"; import { useTooltip } from "@/scripts/use-tooltip";
import { $i } from "@/account"; import { $i } from "@/account";
import { packed } from "magnetar-common";
import { magReactionSelf } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
reaction: string; reaction: string;
count: number; count: number;
isInitial: boolean; isInitial: boolean;
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = ref<HTMLElement>();
@ -43,7 +45,7 @@ const canToggle = computed(() => !props.reaction.match(/@\w/) && $i);
const toggleReaction = () => { const toggleReaction = () => {
if (!canToggle.value) return; if (!canToggle.value) return;
const oldReaction = props.note.myReaction; const oldReaction = magReactionSelf(props.note);
if (oldReaction) { if (oldReaction) {
os.api("notes/reactions/delete", { os.api("notes/reactions/delete", {
noteId: props.note.id, noteId: props.note.id,

View File

@ -1,11 +1,13 @@
<template> <template>
<div class="tdflqwzn" :class="{ isMe }"> <div class="tdflqwzn" :class="{ isMe }">
<XReaction <XReaction
v-for="(count, reaction) in note.reactions" v-for="r in Array.isArray(note.reactions)
:key="reaction" ? note.reactions
:reaction="reaction" : Object.entries(note.reactions)"
:count="count" :key="magReactionPairToLegacy(r)[0]"
:is-initial="initialReactions.has(reaction)" :reaction="magReactionPairToLegacy(r)[0]"
:count="magReactionPairToLegacy(r)[1]"
:is-initial="initialReactions.has(magReactionPairToLegacy(r)[0])"
:note="note" :note="note"
/> />
</div> </div>
@ -16,14 +18,16 @@ import { computed } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { $i } from "@/account"; import { $i } from "@/account";
import XReaction from "@/components/MkReactionsViewer.reaction.vue"; import XReaction from "@/components/MkReactionsViewer.reaction.vue";
import { packed } from "magnetar-common";
import { magReactionPairToLegacy } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
}>(); }>();
const initialReactions = new Set(Object.keys(props.note.reactions)); const initialReactions = new Set(Object.keys(props.note.reactions));
const isMe = computed(() => $i && $i.id === props.note.userId); const isMe = computed(() => $i && $i.id === props.note.user.id);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -23,7 +23,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import type * as misskey from "calckey-js";
import Ripple from "@/components/MkRipple.vue"; import Ripple from "@/components/MkRipple.vue";
import XDetails from "@/components/MkUsersTooltip.vue"; import XDetails from "@/components/MkUsersTooltip.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
@ -33,21 +32,37 @@ import { useTooltip } from "@/scripts/use-tooltip";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { MenuItem } from "@/types/menu"; import { MenuItem } from "@/types/menu";
import { packed } from "magnetar-common";
import {
magLegacyVisibility,
magTransMap,
magTransProperty,
} from "@/scripts-mag/mag-util";
import * as Misskey from "calckey-js";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | Misskey.entities.Note;
count: number; count: number;
detailedView?; detailedView?;
}>(); }>();
const buttonRef = ref<HTMLElement>(); const buttonRef = ref<HTMLElement>();
const hasRenotedBefore = ref(props.note.hasRenotedBefore ?? false); const hasRenotedBefore = ref<boolean>(
magTransMap(
props.note,
"self_renote_count",
"hasRenotedBefore",
(n) => (n ?? 0) > 0
) ?? false
);
const canRenote = computed( const canRenote = computed(
() => () =>
["public", "home"].includes(props.note.visibility) || ["public", "home"].includes(props.note.visibility) ||
props.note.userId === $i.id ($i &&
magTransMap(props.note, "user", "userId", (user) => user.id) ===
$i.id)
); );
useTooltip(buttonRef, async (showing) => { useTooltip(buttonRef, async (showing) => {
@ -141,7 +156,12 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
os.api("notes/create", { os.api("notes/create", {
renoteId: props.note.id, renoteId: props.note.id,
visibility: "specified", visibility: "specified",
visibleUserIds: props.note.visibleUserIds, visibleUserIds:
magTransProperty(
props.note,
"visible_user_ids",
"visibleUserIds"
) ?? [],
}); });
hasRenotedBefore.value = true; hasRenotedBefore.value = true;
const el = const el =
@ -193,16 +213,25 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
action: () => { action: () => {
os.api( os.api(
"notes/create", "notes/create",
props.note.visibility === "specified" magLegacyVisibility(props.note.visibility) === "specified"
? { ? {
renoteId: props.note.id, renoteId: props.note.id,
visibility: props.note.visibility, visibility: magLegacyVisibility(
visibleUserIds: props.note.visibleUserIds, props.note.visibility
),
visibleUserIds:
magTransProperty(
props.note,
"visible_user_ids",
"visibleUserIds"
) ?? [],
localOnly: true, localOnly: true,
} }
: { : {
renoteId: props.note.id, renoteId: props.note.id,
visibility: props.note.visibility, visibility: magLegacyVisibility(
props.note.visibility
),
localOnly: true, localOnly: true,
} }
); );

View File

@ -45,9 +45,10 @@ import * as os from "@/os";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { instance } from "@/instance"; import { instance } from "@/instance";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
note: Note; note: packed.PackNoteBase | Note;
}>(); }>();
function star(ev?: MouseEvent): void { function star(ev?: MouseEvent): void {

View File

@ -47,13 +47,13 @@ import Ripple from "@/components/MkRipple.vue";
import XDetails from "@/components/MkUsersTooltip.vue"; import XDetails from "@/components/MkUsersTooltip.vue";
import { pleaseLogin } from "@/scripts/please-login"; import { pleaseLogin } from "@/scripts/please-login";
import * as os from "@/os"; import * as os from "@/os";
import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { instance } from "@/instance"; import { instance } from "@/instance";
import { useTooltip } from "@/scripts/use-tooltip"; import { useTooltip } from "@/scripts/use-tooltip";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
note: Note; note: packed.PackNoteBase | Note;
count: number; count: number;
reacted: boolean; reacted: boolean;
}>(); }>();

View File

@ -1,7 +1,11 @@
<template> <template>
<p v-if="note.cw != null" class="cw"> <p v-if="note.cw != null" class="cw">
<MkA <MkA
v-if="conversation && note.renoteId == parentId" v-if="
conversation &&
magTransProperty(note, 'renoted_note_id', 'renoteId') ==
parentId
"
:to=" :to="
detailedView ? `#${parentId}` : `${notePage(note)}#${parentId}` detailedView ? `#${parentId}` : `${notePage(note)}#${parentId}`
" "
@ -12,11 +16,17 @@
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</MkA> </MkA>
<MkA <MkA
v-else-if="!detailed && note.replyId" v-else-if="
!detailed && magTransProperty(note, 'parent_note_id', 'replyId')
"
:to=" :to="
detailedView detailedView
? `#${note.replyId}` ? `#${magTransProperty(note, 'parent_note_id', 'replyId')}`
: `${notePage(note)}#${note.replyId}` : `${notePage(note)}#${magTransProperty(
note,
'parent_note_id',
'replyId'
)}`
" "
behavior="browser" behavior="browser"
v-tooltip="i18n.ts.jumpToPrevious" v-tooltip="i18n.ts.jumpToPrevious"
@ -40,7 +50,8 @@
:class="{ :class="{
collapsed, collapsed,
isLong, isLong,
manyImages: note.files.length > 4, manyImages:
magTransProperty(note, 'attachments', 'files')?.length > 4,
showContent: note.cw && !showContent, showContent: note.cw && !showContent,
animatedMfm: !disableMfm, animatedMfm: !disableMfm,
}" }"
@ -66,12 +77,16 @@
tabindex: !showContent ? '-1' : null, tabindex: !showContent ? '-1' : null,
}" }"
> >
<span v-if="note.deletedAt" style="opacity: 0.5"
>({{ i18n.ts.deleted }})</span
>
<template v-if="!note.cw"> <template v-if="!note.cw">
<MkA <MkA
v-if="conversation && note.renoteId == parentId" v-if="
conversation &&
magTransProperty(
note,
'renoted_note_id',
'renoteId'
) == parentId
"
:to=" :to="
detailedView detailedView
? `#${parentId}` ? `#${parentId}`
@ -84,11 +99,22 @@
<i class="ph-quotes ph-bold ph-lg"></i> <i class="ph-quotes ph-bold ph-lg"></i>
</MkA> </MkA>
<MkA <MkA
v-else-if="!detailed && note.replyId" v-else-if="
!detailed &&
magTransProperty(note, 'parent_note_id', 'replyId')
"
:to=" :to="
detailedView detailedView
? `#${note.replyId}` ? `#${magTransProperty(
: `${notePage(note)}#${note.replyId}` note,
'parent_note_id',
'replyId'
)}`
: `${notePage(note)}#${magTransProperty(
note,
'parent_note_id',
'replyId'
)}`
" "
behavior="browser" behavior="browser"
v-tooltip="i18n.ts.jumpToPrevious" v-tooltip="i18n.ts.jumpToPrevious"
@ -106,14 +132,24 @@
:custom-emojis="note.emojis" :custom-emojis="note.emojis"
/> />
<MkA <MkA
v-if="!detailed && note.renoteId" v-if="
!detailed &&
magTransProperty(note, 'renoted_note_id', 'renoteId')
"
class="rp" class="rp"
:to="`/notes/${note.renoteId}`" :to="`/notes/${magTransProperty(
note,
'renoted_note_id',
'renoteId'
)}`"
>{{ i18n.ts.quoteAttached }}: ...</MkA >{{ i18n.ts.quoteAttached }}: ...</MkA
> >
<XMediaList <XMediaList
v-if="note.files.length > 0" v-if="
:media-list="note.files" magTransProperty(note, 'attachments', 'files')?.length >
0
"
:media-list="magTransProperty(note, 'attachments', 'files')!"
/> />
<XPoll v-if="note.poll" :note="note" class="poll" /> <XPoll v-if="note.poll" :note="note" class="poll" />
<template v-if="detailed"> <template v-if="detailed">
@ -126,11 +162,18 @@
class="url-preview" class="url-preview"
/> />
<div <div
v-if="note.renote" v-if="magTransProperty(note, 'renoted_note', 'renote')"
class="renote" class="renote"
@click.stop="emit('push', note.renote)" @click.stop="
emit(
'push',
magTransProperty(note, 'renoted_note', 'renote')
)
"
> >
<XNoteSimple :note="note.renote" /> <XNoteSimple
:note="magTransProperty(note, 'renoted_note', 'renote')!"
/>
</div> </div>
</template> </template>
<div <div
@ -192,9 +235,12 @@ import { extractUrlFromMfm } from "@/scripts/extract-url-from-mfm";
import { extractMfmWithAnimation } from "@/scripts/extract-mfm"; import { extractMfmWithAnimation } from "@/scripts/extract-mfm";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { $i } from "@/account";
import { magTransProperty } from "@/scripts-mag/mag-util";
import { packed } from "magnetar-common";
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
parentId?; parentId?;
conversation?; conversation?;
detailed?: boolean; detailed?: boolean;
@ -216,7 +262,8 @@ const isLong =
((props.note.text != null && ((props.note.text != null &&
(props.note.text.split("\n").length > 10 || (props.note.text.split("\n").length > 10 ||
props.note.text.length > 800)) || props.note.text.length > 800)) ||
props.note.files.length > 4); (magTransProperty(props.note, "attachments", "files") ?? []).length >
4);
const collapsed = $ref(props.note.cw == null && isLong); const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text const urls = props.note.text
? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5) ? extractUrlFromMfm(mfm.parse(props.note.text)).slice(0, 5)

View File

@ -1,49 +0,0 @@
<template>
<div v-tooltip="text" class="fzgwjkgc" :class="user.onlineStatus"></div>
</template>
<script lang="ts" setup>
import {} from "vue";
import * as misskey from "calckey-js";
import { i18n } from "@/i18n";
const props = defineProps<{
user: misskey.entities.User;
}>();
const text = $computed(() => {
switch (props.user.onlineStatus) {
case "online":
return i18n.ts.online;
case "active":
return i18n.ts.active;
case "offline":
return i18n.ts.offline;
case "unknown":
return i18n.ts.unknown;
}
});
</script>
<style lang="scss" scoped>
.fzgwjkgc {
box-shadow: 0 0 0 3px var(--panel);
border-radius: 120%; // Blink100%
&.online {
background: #9ccfd8;
}
&.active {
background: #f6c177;
}
&.offline {
background: #eb6f92;
}
&.unknown {
background: #6e6a86;
}
}
</style>

View File

@ -13,9 +13,10 @@
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { toUnicode } from "punycode/"; import { toUnicode } from "punycode/";
import { host as hostRaw } from "@/config"; import { host as hostRaw } from "@/config";
import { packed } from "magnetar-common";
defineProps<{ defineProps<{
user: misskey.entities.UserDetailed; user: packed.PackUserBase | misskey.entities.User;
detail?: boolean; detail?: boolean;
}>(); }>();

View File

@ -3,23 +3,36 @@
v-if="disableLink" v-if="disableLink"
v-user-preview="disablePreview ? undefined : user.id" v-user-preview="disablePreview ? undefined : user.id"
class="eiwwqkts _noSelect" class="eiwwqkts _noSelect"
:class="{ cat: user.isCat, square: $store.state.squareAvatars }" :class="{
cat:
magTransMap(
props.user,
'avatar_decoration',
'isCat',
(a) => a === 'CatEars'
) || false,
square: $store.state.squareAvatars,
}"
:style="{ color }" :style="{ color }"
:title="acct(user)" :title="acct(user)"
@click="onClick" @click="onClick"
> >
<img class="inner" :src="url" decoding="async" /> <img class="inner" :src="url" decoding="async" />
<MkUserOnlineIndicator
v-if="showIndicator && user.instance == null"
class="indicator"
:user="user"
/>
</span> </span>
<MkA <MkA
v-else v-else
v-user-preview="disablePreview ? undefined : user.id" v-user-preview="disablePreview ? undefined : user.id"
class="eiwwqkts _noSelect" class="eiwwqkts _noSelect"
:class="{ cat: user.isCat, square: $store.state.squareAvatars }" :class="{
cat:
magTransMap(
props.user,
'avatar_decoration',
'isCat',
(d) => d === 'CatEars'
) || false,
square: $store.state.squareAvatars,
}"
:style="{ color }" :style="{ color }"
:to="userPage(user)" :to="userPage(user)"
:title="acct(user)" :title="acct(user)"
@ -27,36 +40,30 @@
@click.stop @click.stop
> >
<img class="inner" :src="url" decoding="async" /> <img class="inner" :src="url" decoding="async" />
<MkUserOnlineIndicator
v-if="showIndicator && user.instance == null"
class="indicator"
:user="user"
/>
</MkA> </MkA>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, watch } from "vue"; import { watch } from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { getStaticImageUrl } from "@/scripts/get-static-image-url"; import { getStaticImageUrl } from "@/scripts/get-static-image-url";
import { extractAvgColorFromBlurhash } from "@/scripts/extract-avg-color-from-blurhash"; import { extractAvgColorFromBlurhash } from "@/scripts/extract-avg-color-from-blurhash";
import { acct, userPage } from "@/filters/user"; import { acct, userPage } from "@/filters/user";
import MkUserOnlineIndicator from "@/components/MkUserOnlineIndicator.vue";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { packed } from "magnetar-common";
import { magTransMap, magTransProperty } from "@/scripts-mag/mag-util";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
user: misskey.entities.User; user: packed.PackUserBase | misskey.entities.User;
target?: string | null; target?: string | null;
disableLink?: boolean; disableLink?: boolean;
disablePreview?: boolean; disablePreview?: boolean;
showIndicator?: boolean;
}>(), }>(),
{ {
target: null, target: null,
disableLink: false, disableLink: false,
disablePreview: false, disablePreview: false,
showIndicator: false,
} }
); );
@ -66,8 +73,10 @@ const emit = defineEmits<{
const url = $computed(() => const url = $computed(() =>
defaultStore.state.disableShowingAnimatedImages defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.user.avatarUrl) ? getStaticImageUrl(
: props.user.avatarUrl magTransProperty(props.user, "avatar_url", "avatarUrl")
)
: magTransProperty(props.user, "avatar_url", "avatarUrl")
); );
function onClick(ev: MouseEvent) { function onClick(ev: MouseEvent) {
@ -77,9 +86,11 @@ function onClick(ev: MouseEvent) {
let color = $ref(); let color = $ref();
watch( watch(
() => props.user.avatarBlurhash, () => magTransProperty(props.user, "avatar_blurhash", "avatarBlurhash"),
() => { () => {
color = extractAvgColorFromBlurhash(props.user.avatarBlurhash); color = extractAvgColorFromBlurhash(
magTransProperty(props.user, "avatar_blurhash", "avatarBlurhash")
);
}, },
{ {
immediate: true, immediate: true,

View File

@ -1,7 +1,7 @@
<template> <template>
<Mfm <Mfm
:class="$style.root" :class="$style.root"
:text="user.name || user.username" :text="magTransUsername(user)"
:plain="true" :plain="true"
:nowrap="nowrap" :nowrap="nowrap"
:custom-emojis="user.emojis" :custom-emojis="user.emojis"
@ -9,12 +9,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {} from "vue";
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { packed } from "magnetar-common";
import { magTransUsername } from "@/scripts-mag/mag-util";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
user: misskey.entities.User; user: packed.PackUserBase | misskey.entities.User;
nowrap?: boolean; nowrap?: boolean;
}>(), }>(),
{ {

View File

@ -15,10 +15,11 @@
<script lang="ts"> <script lang="ts">
import { defineComponent, onMounted, PropType, Ref, ref } from "vue"; import { defineComponent, onMounted, PropType, Ref, ref } from "vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MagNote.vue";
import XNoteDetailed from "@/components/MkNoteDetailed.vue"; import XNoteDetailed from "@/components/MagNoteDetailed.vue";
import * as os from "@/os"; import * as os from "@/os";
import { NoteBlock } from "@/scripts/hpml/block"; import { NoteBlock } from "@/scripts/hpml/block";
import { endpoints, packed } from "magnetar-common";
export default defineComponent({ export default defineComponent({
components: { components: {
@ -32,14 +33,18 @@ export default defineComponent({
}, },
}, },
setup(props, ctx) { setup(props, ctx) {
const note: Ref<Record<string, any> | null> = ref(null); const note: Ref<packed.PackNoteMaybeFull | null> = ref(null);
onMounted(() => { onMounted(() => {
os.api("notes/show", { noteId: props.block.note }).then( if (!props.block.note) return;
(result) => {
note.value = result; os.magApi(
} endpoints.GetNoteById,
); { context: props.block.detailed, attachments: true },
{ id: props.block.note }
).then((result) => {
note.value = result;
});
}); });
return { return {

View File

@ -15,6 +15,6 @@ export const lang = localStorage.getItem("lang");
export const langs = _LANGS_; export const langs = _LANGS_;
export const locale = JSON.parse(localStorage.getItem("locale")); export const locale = JSON.parse(localStorage.getItem("locale"));
export const version = _VERSION_; export const version = _VERSION_;
export const instanceName = siteName === "Magnetar" ? host : siteName; export const instanceName = siteName === "Magnetar" ? "" + host : siteName;
export const ui = localStorage.getItem("ui"); export const ui = localStorage.getItem("ui");
export const debug = localStorage.getItem("debug") === "true"; export const debug = localStorage.getItem("debug") === "true";

View File

@ -1,3 +1,8 @@
export const notePage = (note) => { import * as Misskey from "calckey-js";
import { packed } from "magnetar-common";
export const notePage = (note: {
id: packed.PackNoteBase["id"] | Misskey.entities.Note["id"];
}) => {
return `/notes/${note.id}`; return `/notes/${note.id}`;
}; };

View File

@ -11,36 +11,28 @@ import "@phosphor-icons/web/fill";
//#region account indexedDB migration //#region account indexedDB migration
import { set } from "@/scripts/idb-proxy"; import { set } from "@/scripts/idb-proxy";
const accounts = localStorage.getItem("accounts");
if (accounts) {
set("accounts", JSON.parse(accounts));
localStorage.removeItem("accounts");
}
//#endregion
import { import {
computed, computed,
createApp, createApp,
watch, defineAsyncComponent,
markRaw, markRaw,
version as vueVersion, version as vueVersion,
defineAsyncComponent, watch,
} from "vue"; } from "vue";
import { compareVersions } from "compare-versions"; import { compareVersions } from "compare-versions";
import widgets from "@/widgets"; import widgets from "@/widgets";
import directives from "@/directives"; import directives from "@/directives";
import components from "@/components"; import components from "@/components";
import { version, ui, lang, host } from "@/config"; import { lang, ui, version } from "@/config";
import { applyTheme } from "@/scripts/theme"; import { applyTheme } from "@/scripts/theme";
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode"; import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { confirm, alert, post, popup, toast } from "@/os"; import { alert, confirm, popup, post, toast } from "@/os";
import { stream } from "@/stream"; import { stream } from "@/stream";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import { $i, refreshAccount, login, updateAccount, signout } from "@/account"; import { $i, login, refreshAccount, signout, updateAccount } from "@/account";
import { defaultStore, ColdDeviceStorage } from "@/store"; import { ColdDeviceStorage, defaultStore } from "@/store";
import { fetchInstance, instance } from "@/instance"; import { fetchInstance, instance } from "@/instance";
import { makeHotkey } from "@/scripts/hotkey"; import { makeHotkey } from "@/scripts/hotkey";
import { search } from "@/scripts/search"; import { search } from "@/scripts/search";
@ -51,6 +43,13 @@ import { reactionPicker } from "@/scripts/reaction-picker";
import { getUrlWithoutLoginId } from "@/scripts/login-id"; import { getUrlWithoutLoginId } from "@/scripts/login-id";
import { getAccountFromId } from "@/scripts/get-account-from-id"; import { getAccountFromId } from "@/scripts/get-account-from-id";
const accounts = localStorage.getItem("accounts");
if (accounts) {
set("accounts", JSON.parse(accounts));
localStorage.removeItem("accounts");
}
//#endregion
function checkForSplash() { function checkForSplash() {
const splash = document.getElementById("splash"); const splash = document.getElementById("splash");
// 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す))
@ -64,7 +63,7 @@ function checkForSplash() {
} }
(async () => { (async () => {
console.info(`Calckey v${version}`); console.info(`Magnetar v${version}`);
if (_DEV_) { if (_DEV_) {
console.warn("Development mode!!!"); console.warn("Development mode!!!");

View File

@ -13,9 +13,12 @@ import { MenuItem } from "@/types/menu";
import { $i } from "@/account"; import { $i } from "@/account";
import { i18n } from "./i18n"; import { i18n } from "./i18n";
import { import {
BackendApiEndpoint,
FrontendApiEndpoint, FrontendApiEndpoint,
FrontendApiEndpoints, FrontendApiEndpoints,
} from "magnetar-common/built/fe-api"; MagApiClient,
Method,
} from "magnetar-common";
export const pendingApiRequestsCount = ref(0); export const pendingApiRequestsCount = ref(0);
@ -23,6 +26,39 @@ const apiClient = new Misskey.api.APIClient({
origin: url, origin: url,
}); });
const magnetarApiClient = new MagApiClient(`${url}/mag/v1`);
export async function magApi<
T extends BackendApiEndpoint<
T["method"] & Method,
T["pathParams"] & string[],
T["request"],
T["response"]
>
>(
endpoint: T,
data: T["request"],
pathParams: {
[K in keyof T["pathParams"] as T["pathParams"][K] & string]:
| string
| number;
},
token?: string | null | undefined
): Promise<T["response"]> {
pendingApiRequestsCount.value++;
try {
return await magnetarApiClient.call(
endpoint,
data,
pathParams,
token || $i?.token
);
} finally {
pendingApiRequestsCount.value--;
}
}
export async function feApi<T extends keyof FrontendApiEndpoints & string>( export async function feApi<T extends keyof FrontendApiEndpoints & string>(
endpointDef: FrontendApiEndpoint< endpointDef: FrontendApiEndpoint<
FrontendApiEndpoints[T]["method"], FrontendApiEndpoints[T]["method"],

View File

@ -34,7 +34,7 @@
<div class="note _gap"> <div class="note _gap">
<MkRemoteCaution <MkRemoteCaution
v-if="appearNote.user.host != null" v-if="appearNote.user.host != null"
:href="appearNote.url ?? appearNote.uri" :href="(appearNote.url ?? appearNote.uri) !"
/> />
<XNoteDetailed <XNoteDetailed
:key="appearNote.id" :key="appearNote.id"
@ -70,27 +70,28 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed, watch } from "vue"; import { computed, watch } from "vue";
import * as misskey from "calckey-js"; import XNoteDetailed from "@/components/MagNoteDetailed.vue";
import XNoteDetailed from "@/components/MkNoteDetailed.vue";
import XNotes from "@/components/MkNotes.vue"; import XNotes from "@/components/MkNotes.vue";
import MkRemoteCaution from "@/components/MkRemoteCaution.vue"; import MkRemoteCaution from "@/components/MkRemoteCaution.vue";
import MkButton from "@/components/MkButton.vue"; import MkButton from "@/components/MkButton.vue";
import * as os from "@/os"; import * as os from "@/os";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { endpoints, packed } from "magnetar-common";
import { magTransUsername } from "@/scripts-mag/mag-util";
const props = defineProps<{ const props = defineProps<{
noteId: string; noteId: string;
}>(); }>();
let note = $ref<null | misskey.entities.Note>(); let note = $ref<null | packed.PackNoteMaybeFull>();
let hasPrev = $ref(false); let hasPrev = $ref(false);
let hasNext = $ref(false); let hasNext = $ref(false);
let showPrev = $ref(false); let showPrev = $ref(false);
let showNext = $ref(false); let showNext = $ref(false);
let error = $ref(); let error = $ref();
let isRenote = $ref(false); let isRenote = $ref(false);
let appearNote = $ref<null | misskey.entities.Note>(); let appearNote = $ref<null | packed.PackNoteMaybeFull>();
const prevPagination = { const prevPagination = {
endpoint: "users/notes" as const, endpoint: "users/notes" as const,
@ -98,7 +99,7 @@ const prevPagination = {
params: computed(() => params: computed(() =>
appearNote appearNote
? { ? {
userId: appearNote.userId, userId: appearNote.user.id,
untilId: appearNote.id, untilId: appearNote.id,
} }
: null : null
@ -112,7 +113,7 @@ const nextPagination = {
params: computed(() => params: computed(() =>
appearNote appearNote
? { ? {
userId: appearNote.userId, userId: appearNote.user.id,
sinceId: appearNote.id, sinceId: appearNote.id,
} }
: null : null
@ -125,28 +126,30 @@ function fetchNote() {
showPrev = false; showPrev = false;
showNext = false; showNext = false;
note = null; note = null;
os.api("notes/show", { os.magApi(
noteId: props.noteId, endpoints.GetNoteById,
}) { context: true, attachments: true },
{ id: props.noteId }
)
.then((res) => { .then((res) => {
note = res; note = res;
isRenote = isRenote =
note.renote != null && note.renoted_note_id != null &&
note.text == null && note.text == null &&
note.fileIds.length === 0 && note.file_ids.length === 0 &&
note.poll == null; note.poll == null;
appearNote = isRenote appearNote = isRenote
? (note.renote as misskey.entities.Note) ? (note.renoted_note as packed.PackNoteMaybeFull)
: note; : note;
Promise.all([ Promise.all([
os.api("users/notes", { os.api("users/notes", {
userId: note.userId, userId: note.user.id,
untilId: note.id, untilId: note.id,
limit: 1, limit: 1,
}), }),
os.api("users/notes", { os.api("users/notes", {
userId: note.userId, userId: note.user.id,
sinceId: note.id, sinceId: note.id,
limit: 1, limit: 1,
}), }),
@ -173,15 +176,14 @@ definePageMetadata(
appearNote appearNote
? { ? {
title: i18n.t("noteOf", { title: i18n.t("noteOf", {
user: appearNote.user.name || appearNote.user.username, user: magTransUsername(appearNote.user),
}), }),
subtitle: new Date(appearNote.createdAt).toLocaleString(), subtitle: new Date(appearNote.created_at).toLocaleString(),
avatar: appearNote.user, avatar: appearNote.user,
path: `/notes/${appearNote.id}`, path: `/notes/${appearNote.id}`,
share: { share: {
title: i18n.t("noteOf", { title: i18n.t("noteOf", {
user: user: magTransUsername(appearNote.user),
appearNote.user.name || appearNote.user.username,
}), }),
text: appearNote.text, text: appearNote.text,
}, },

View File

@ -35,14 +35,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { watch } from "vue"; import { ref, watch } from "vue";
import XContainer from "../page-editor.container.vue"; import XContainer from "../page-editor.container.vue";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
import MkSwitch from "@/components/form/switch.vue"; import MkSwitch from "@/components/form/switch.vue";
import XNote from "@/components/MkNote.vue"; import XNote from "@/components/MkNote.vue";
import XNoteDetailed from "@/components/MkNoteDetailed.vue"; import XNoteDetailed from "@/components/MagNoteDetailed.vue";
import * as os from "@/os"; import * as os from "@/os";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { endpoints, packed } from "magnetar-common";
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@ -56,21 +57,32 @@ const props = withDefaults(
} }
); );
let id: any = $ref(props.value.note); let id = ref<string | null>(props.value.note);
let note: any = $ref(null); let note = ref<packed.PackNoteMaybeFull | null>(null);
watch( watch(
id, id,
async () => { async () => {
if (id && (id.startsWith("http://") || id.startsWith("https://"))) { if (
props.value.note = (id.endsWith("/") ? id.slice(0, -1) : id) id.value &&
(id.value.startsWith("http://") || id.value.startsWith("https://"))
) {
props.value.note = (
id.value.endsWith("/") ? id.value.slice(0, -1) : id.value
)
.split("/") .split("/")
.pop(); .pop();
} else { } else {
props.value.note = id; props.value.note = id.value;
} }
note = await os.api("notes/show", { noteId: props.value.note }); if (props.value.note) return;
note.value = await os.magApi(
endpoints.GetNoteById,
{ context: true, attachments: true },
{ id: props.value.note }
);
}, },
{ {
immediate: true, immediate: true,

View File

@ -125,7 +125,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, watch } from "vue"; import { defineAsyncComponent, watch } from "vue";
import XDraggable from "vuedraggable"; import XDraggable from "vuedraggable";
import FormInput from "@/components/form/input.vue";
import FormRadios from "@/components/form/radios.vue"; import FormRadios from "@/components/form/radios.vue";
import FromSlot from "@/components/form/slot.vue"; import FromSlot from "@/components/form/slot.vue";
import FormButton from "@/components/MkButton.vue"; import FormButton from "@/components/MkButton.vue";
@ -135,7 +134,6 @@ import * as os from "@/os";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { definePageMetadata } from "@/scripts/page-metadata"; import { definePageMetadata } from "@/scripts/page-metadata";
import { deepClone } from "@/scripts/clone";
import { unisonReload } from "@/scripts/unison-reload"; import { unisonReload } from "@/scripts/unison-reload";
async function reloadAsk() { async function reloadAsk() {
@ -148,7 +146,7 @@ async function reloadAsk() {
unisonReload(); unisonReload();
} }
let reactions = $ref(deepClone(defaultStore.state.reactions)); let reactions = $ref(structuredClone(defaultStore.state.reactions));
const reactionPickerSkinTone = $computed( const reactionPickerSkinTone = $computed(
defaultStore.makeGetterSetter("reactionPickerSkinTone") defaultStore.makeGetterSetter("reactionPickerSkinTone")
@ -211,7 +209,7 @@ async function setDefault() {
}); });
if (canceled) return; if (canceled) return;
reactions = deepClone(defaultStore.def.reactions.default); reactions = structuredClone(defaultStore.def.reactions.default);
} }
function chooseEmoji(ev: MouseEvent) { function chooseEmoji(ev: MouseEvent) {

View File

@ -142,17 +142,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, reactive, ref, watch } from "vue"; import { reactive, watch } from "vue";
import FormSelect from "@/components/form/select.vue"; import FormSelect from "@/components/form/select.vue";
import MkInput from "@/components/form/input.vue"; import MkInput from "@/components/form/input.vue";
import MkSwitch from "@/components/form/switch.vue"; import MkSwitch from "@/components/form/switch.vue";
import FormRadios from "@/components/form/radios.vue"; import FormRadios from "@/components/form/radios.vue";
import FormButton from "@/components/MkButton.vue"; import FormButton from "@/components/MkButton.vue";
import FormRange from "@/components/form/range.vue"; import FormRange from "@/components/form/range.vue";
import * as os from "@/os";
import { defaultStore } from "@/store"; import { defaultStore } from "@/store";
import { i18n } from "@/i18n"; import { i18n } from "@/i18n";
import { deepClone } from "@/scripts/clone";
const props = defineProps<{ const props = defineProps<{
_id: string; _id: string;
@ -160,7 +158,9 @@ const props = defineProps<{
}>(); }>();
const statusbar = reactive( const statusbar = reactive(
deepClone(defaultStore.state.statusbars.find((x) => x.id === props._id)) structuredClone(
defaultStore.state.statusbars.find((x) => x.id === props._id)
)
); );
watch( watch(
@ -197,8 +197,8 @@ async function save() {
const i = defaultStore.state.statusbars.findIndex( const i = defaultStore.state.statusbars.findIndex(
(x) => x.id === props._id (x) => x.id === props._id
); );
const statusbars = deepClone(defaultStore.state.statusbars); const statusbars = structuredClone(defaultStore.state.statusbars);
statusbars[i] = deepClone(statusbar); statusbars[i] = structuredClone(statusbar);
defaultStore.set("statusbars", statusbars); defaultStore.set("statusbars", statusbars);
} }

View File

@ -135,7 +135,7 @@ function registerPostFormAction({ pluginId, title, handler }) {
postFormActions.push({ postFormActions.push({
title, title,
handler: (form, update) => { handler: (form, update) => {
pluginContexts.get(pluginId).execFn(handler, [ pluginContexts.get(pluginId)?.execFn(handler, [
utils.jsToVal(form), utils.jsToVal(form),
values.FN_NATIVE(([key, value]) => { values.FN_NATIVE(([key, value]) => {
update(key.value, value.value); update(key.value, value.value);
@ -149,7 +149,9 @@ function registerUserAction({ pluginId, title, handler }) {
userActions.push({ userActions.push({
title, title,
handler: (user) => { handler: (user) => {
pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]); pluginContexts
.get(pluginId)
?.execFn(handler, [utils.jsToVal(user)]);
}, },
}); });
} }
@ -158,7 +160,9 @@ function registerNoteAction({ pluginId, title, handler }) {
noteActions.push({ noteActions.push({
title, title,
handler: (note) => { handler: (note) => {
pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]); pluginContexts
.get(pluginId)
?.execFn(handler, [utils.jsToVal(note)]);
}, },
}); });
} }

View File

@ -0,0 +1,240 @@
import * as Misskey from "calckey-js";
import { packed, types } from "magnetar-common";
// https://stackoverflow.com/a/50375286 Evil magic
type Dist<U> = U extends any ? (k: U) => void : never;
type UnionToIntersection<U> = Dist<U> extends (k: infer I) => void ? I : never;
// End of evil magic
type UnionMerged<A> = { [AK in keyof A]: AK extends keyof A ? A[AK] : never };
type UnionIntersectionMerge<A> = Omit<
UnionToIntersection<A>,
keyof UnionMerged<A>
> &
UnionMerged<A>;
export function magTransProperty<
A extends Record<string, any>,
AA extends keyof UnionToIntersection<A> & string,
BB extends keyof UnionToIntersection<A> & string
>(
x: A,
keyA: AA,
keyB: BB
): UnionIntersectionMerge<A>[AA] | UnionIntersectionMerge<A>[BB] {
const a = x[keyA];
if (typeof a !== "undefined") {
return a;
}
return x[keyB];
}
export function magTransMap<
A extends Record<string, any>,
AA extends keyof UnionIntersectionMerge<A> & string,
BB extends keyof UnionIntersectionMerge<A> & string,
X extends any
>(
x: A,
keyA: AA,
keyB: BB,
mapLeft: (a: UnionIntersectionMerge<A>[AA]) => X = (a) => a as X,
mapRight: (b: UnionIntersectionMerge<A>[BB]) => X = (a) => a as X
): X {
const a = x[keyA];
if (typeof a !== "undefined") {
return mapLeft(a as UnionIntersectionMerge<A>[AA]);
}
const b = x[keyB];
return typeof b === "undefined"
? b
: mapRight(b as UnionIntersectionMerge<A>[BB]);
}
type UserUnion = packed.PackUserBase | Misskey.entities.User;
export function magTransUsername(x: UserUnion): string {
return (
(x as UnionToIntersection<UserUnion>)["display_name"] ||
(x as UnionToIntersection<UserUnion>)["name"] ||
(x as UnionToIntersection<UserUnion>)["username"]
);
}
export function magReactionCount(
note: packed.PackNoteMaybeFull | Misskey.entities.Note
): number {
if (Array.isArray(note.reactions)) {
return Number(
note.reactions
.map(([, cnt]) => cnt)
.reduce((partialSum, val) => partialSum + val, 0)
);
} else {
return Object.values(note).reduce((accum, val) => accum + val, 0);
}
}
export function magHasReacted(
note: packed.PackNoteMaybeFull | Misskey.entities.Note
): boolean {
if (Array.isArray(note.reactions)) {
return note.reactions.some(([, , reacted]) => reacted === true);
} else {
return (
typeof (note as Misskey.entities.Note).myReaction !== "undefined" &&
(note as Misskey.entities.Note).myReaction !== null
);
}
}
export function magReactionSelf(
note: packed.PackNoteMaybeFull | Misskey.entities.Note
): string | null {
if (Array.isArray(note.reactions)) {
const found = note.reactions.find(
([, , reacted]) => reacted === true
)?.[0];
return typeof found !== "undefined" ? magReactionToLegacy(found) : null;
} else if ((note as Misskey.entities.Note).myReaction !== "undefined") {
return (note as Misskey.entities.Note).myReaction ?? null;
}
return null;
}
export function noteIsMag(
note: packed.PackNoteMaybeFull | Misskey.entities.Note
): note is packed.PackNoteMaybeFull {
return "created_at" in note;
}
export function magIsRenote(
note: packed.PackNoteMaybeFull | Misskey.entities.Note
): boolean {
return (
magTransProperty(note, "renoted_note", "renote") != null &&
note.text == null &&
magTransProperty(note, "file_ids", "fileIds").length === 0 &&
note.poll == null
);
}
export function magEffectiveNote(
note: packed.PackNoteMaybeFull | Misskey.entities.Note
): packed.PackNoteMaybeFull | Misskey.entities.Note {
return magIsRenote(note)
? (magTransProperty(note, "renoted_note", "renote") as
| packed.PackNoteMaybeFull
| Misskey.entities.Note)
: note;
}
export function magLegacyVisibility(
vis: types.NoteVisibility | Misskey.entities.Note["visibility"]
): Misskey.entities.Note["visibility"];
export function magLegacyVisibility(vis: undefined): undefined;
export function magLegacyVisibility(
vis: types.NoteVisibility | Misskey.entities.Note["visibility"] | undefined
): Misskey.entities.Note["visibility"] | undefined {
if (typeof vis === "undefined") return vis;
switch (vis) {
case "Public":
return "public";
case "Home":
return "home";
case "Followers":
return "followers";
case "Direct":
return "specified";
case "public":
case "home":
case "followers":
case "specified":
return vis;
}
}
export function magConvertReaction(
reaction: string,
urlHint?: string | null
): types.Reaction {
if (reaction.endsWith("@.:")) {
reaction = reaction.replaceAll(/@\.:$/, ":");
}
if (reaction.match(/^:.+:$/)) {
const [name, host] = reaction.split("@");
return {
name,
host: host || null,
url: urlHint!,
};
} else {
return reaction;
}
}
export function magReactionToLegacy(reaction: types.Reaction | string): string {
if (typeof reaction === "string") {
return reaction;
}
if ("name" in reaction) {
if (reaction.host) {
return `:${reaction.name}@${reaction.host}:`;
} else {
return `:${reaction.name}@.:`;
}
}
if ("raw" in reaction) {
return reaction.raw;
}
return reaction;
}
export function magReactionPairToLegacy(
reaction: types.ReactionPair | [string, number]
): [string, number] {
const legacy = magReactionToLegacy(reaction[0]);
return [legacy, reaction[1]];
}
export function magReactionIndex(
reactions: types.ReactionPair[],
reactionType: types.Reaction
) {
return reactions.findIndex(([r, ,]) => {
if (typeof r !== typeof reactionType) return false;
if (typeof r === "string") {
return r === reactionType;
} else if (
"name" in r &&
"name" in (reactionType as { name: string; host: string | null })
) {
const { name, host } = reactionType as {
name: string;
host: string | null;
};
const { name: rName, host: rHost } = r;
return name === rName && (host ?? null) === (rHost ?? null);
} else if ("raw" in r && "raw" in (reactionType as { raw: string })) {
return r.raw === (reactionType as { raw: string }).raw;
}
return false;
});
}

View File

@ -1,23 +1,28 @@
import { packed } from "magnetar-common";
import * as Misskey from "calckey-js";
import { magTransMap, magTransProperty } from "@/scripts-mag/mag-util";
export type Muted = { export type Muted = {
muted: boolean; muted: boolean;
matched: string[]; matched: string[];
what?: string; // "note" || "reply" || "renote" || "quote" what?: "note" | "reply" | "renote" | "quote";
}; };
const NotMuted = { muted: false, matched: [] }; export type NotMuted = { muted: false; matched: string[] };
function checkWordMute( function checkWordMute(
note: NoteLike, note: packed.PackNoteMaybeFull | Misskey.entities.Note,
mutedWords: Array<string | string[]> mutedWords: Array<string | string[]>
): Muted { ): Muted {
let text = `${note.cw ?? ""} ${note.text ?? ""}`; let text = `${note.cw ?? ""} ${note.text ?? ""}`;
if (note.files != null) const attachments = magTransProperty(note, "attachments", "files");
text += ` ${note.files.map((f) => f.comment ?? "").join(" ")}`; if (attachments)
text += ` ${attachments.map((f) => f.comment ?? "").join(" ")}`;
text = text.trim(); text = text.trim();
if (text === "") return NotMuted; let result: Muted | NotMuted = { muted: false, matched: [] };
let result = { muted: false, matched: [] }; if (text === "") return result;
for (const mutePattern of mutedWords) { for (const mutePattern of mutedWords) {
if (Array.isArray(mutePattern)) { if (Array.isArray(mutePattern)) {
@ -59,13 +64,13 @@ function checkWordMute(
} }
export function getWordSoftMute( export function getWordSoftMute(
note: Record<string, any>, note: packed.PackNoteMaybeFull | Misskey.entities.Note,
me: Record<string, any> | null | undefined, me: Record<string, any> | null | undefined,
mutedWords: Array<string | string[]> mutedWords: Array<string | string[]>
): Muted { ): Muted {
// 自分自身 // 自分自身
if (me && note.userId === me.id) { if (me && magTransMap(note, "user", "userId", (u) => u.id) === me.id) {
return NotMuted; return { muted: false, matched: [] };
} }
if (mutedWords.length > 0) { if (mutedWords.length > 0) {
@ -75,16 +80,18 @@ export function getWordSoftMute(
return noteMuted; return noteMuted;
} }
if (note.renote) { const renote = magTransProperty(note, "renoted_note", "renote");
let renoteMuted = checkWordMute(note.renote, mutedWords); if (renote) {
let renoteMuted = checkWordMute(renote, mutedWords);
if (renoteMuted.muted) { if (renoteMuted.muted) {
renoteMuted.what = note.text == null ? "renote" : "quote"; renoteMuted.what = note.text == null ? "renote" : "quote";
return renoteMuted; return renoteMuted;
} }
} }
if (note.reply) { const reply = magTransProperty(note, "parent_note", "reply");
let replyMuted = checkWordMute(note.reply, mutedWords); if (reply) {
let replyMuted = checkWordMute(reply, mutedWords);
if (replyMuted.muted) { if (replyMuted.muted) {
replyMuted.what = "reply"; replyMuted.what = "reply";
return replyMuted; return replyMuted;
@ -92,5 +99,5 @@ export function getWordSoftMute(
} }
} }
return NotMuted; return { muted: false, matched: [] };
} }

View File

@ -1,24 +0,0 @@
// structredCloneが遅いため
// SEE: http://var.blog.jp/archives/86038606.html
type Cloneable =
| string
| number
| boolean
| null
| { [key: string]: Cloneable }
| Cloneable[];
export function deepClone<T extends Cloneable>(x: T): T {
if (typeof x === "object") {
if (x === null) return x;
if (Array.isArray(x)) return x.map(deepClone) as T;
const obj = {} as Record<string, Cloneable>;
for (const [k, v] of Object.entries(x)) {
obj[k] = deepClone(v);
}
return obj as T;
} else {
return x;
}
}

View File

@ -1,5 +1,5 @@
export function extractAvgColorFromBlurhash(hash: string) { export function extractAvgColorFromBlurhash(hash: string | null) {
return typeof hash === "string" return hash
? `#${[...hash.slice(2, 6)] ? `#${[...hash.slice(2, 6)]
.map((x) => .map((x) =>
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".indexOf( "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~".indexOf(

View File

@ -9,24 +9,23 @@ import { url } from "@/config";
import { noteActions } from "@/store"; import { noteActions } from "@/store";
import { shareAvailable } from "@/scripts/share-available"; import { shareAvailable } from "@/scripts/share-available";
import { getUserMenu } from "@/scripts/get-user-menu"; import { getUserMenu } from "@/scripts/get-user-menu";
import {
magEffectiveNote,
magTransMap,
magTransProperty,
magTransUsername,
} from "@/scripts-mag/mag-util";
import { packed } from "magnetar-common";
export function getNoteMenu(props: { export function getNoteMenu(props: {
note: misskey.entities.Note; note: packed.PackNoteMaybeFull | misskey.entities.Note;
menuButton: Ref<HTMLElement | undefined>; menuButton: Ref<HTMLElement | undefined>;
translation: Ref<any>; translation: Ref<any>;
translating: Ref<boolean>; translating: Ref<boolean>;
isDeleted: Ref<boolean>; isDeleted: Ref<boolean>;
currentClipPage?: Ref<misskey.entities.Clip>; currentClipPage?: Ref<misskey.entities.Clip>;
}) { }) {
const isRenote = const appearNote = magEffectiveNote(props.note);
props.note.renote != null &&
props.note.text == null &&
props.note.fileIds.length === 0 &&
props.note.poll == null;
const appearNote = isRenote
? (props.note.renote as misskey.entities.Note)
: props.note;
function del(): void { function del(): void {
os.confirm({ os.confirm({
@ -54,8 +53,8 @@ export function getNoteMenu(props: {
os.post({ os.post({
initialNote: appearNote, initialNote: appearNote,
renote: appearNote.renote, renote: magTransProperty(appearNote, "renoted_note", "renote"),
reply: appearNote.reply, reply: magTransProperty(appearNote, "parent_note", "reply"),
}); });
}); });
} }
@ -63,8 +62,8 @@ export function getNoteMenu(props: {
function edit(): void { function edit(): void {
os.post({ os.post({
initialNote: appearNote, initialNote: appearNote,
renote: appearNote.renote, renote: magTransProperty(appearNote, "renoted_note", "renote"),
reply: appearNote.reply, reply: magTransProperty(appearNote, "parent_note", "reply"),
editId: appearNote.id, editId: appearNote.id,
}); });
} }
@ -228,23 +227,12 @@ export function getNoteMenu(props: {
props.isDeleted.value = true; props.isDeleted.value = true;
} }
async function promote(): Promise<void> {
const { canceled, result: days } = await os.inputNumber({
title: i18n.ts.numberOfDays,
});
if (canceled) return;
os.apiWithDialog("admin/promo/create", {
noteId: appearNote.id,
expiresAt: Date.now() + 86400000 * days,
});
}
function share(): void { function share(): void {
navigator.share({ navigator.share({
title: i18n.t("noteOf", { user: appearNote.user.name }), title: i18n.t("noteOf", {
text: appearNote.text, user: magTransUsername(appearNote.user),
}),
text: appearNote.text ?? undefined,
url: `${url}/notes/${appearNote.id}`, url: `${url}/notes/${appearNote.id}`,
}); });
} }
@ -266,7 +254,8 @@ export function getNoteMenu(props: {
noteId: appearNote.id, noteId: appearNote.id,
}); });
const isAppearAuthor = appearNote.userId === $i.id; const isAppearAuthor =
magTransMap(appearNote, "user", "userId", (u) => u.id) === $i.id;
const isModerator = $i.isAdmin || $i.isModerator; const isModerator = $i.isAdmin || $i.isModerator;
menu = [ menu = [
@ -353,7 +342,7 @@ export function getNoteMenu(props: {
text: i18n.ts.showOnRemote, text: i18n.ts.showOnRemote,
action: () => { action: () => {
window.open( window.open(
appearNote.url || appearNote.uri, (appearNote.url || appearNote.uri)!,
"_blank" "_blank"
); );
}, },
@ -468,7 +457,7 @@ export function getNoteMenu(props: {
text: i18n.ts.showOnRemote, text: i18n.ts.showOnRemote,
action: () => { action: () => {
window.open( window.open(
appearNote.url || appearNote.uri, (appearNote.url || appearNote.uri)!,
"_blank" "_blank"
); );
}, },

View File

@ -1,11 +1,14 @@
import * as misskey from "calckey-js"; import * as misskey from "calckey-js";
import { i18n } from "@/i18n"; import { packed } from "magnetar-common";
import { magTransProperty } from "@/scripts-mag/mag-util";
/** /**
* 稿 * 稿
* @param {*} note (packされた)稿 * @param {*} note (packされた)稿
*/ */
export const getNoteSummary = (note: misskey.entities.Note): string => { export const getNoteSummary = (
note: packed.PackNoteMaybeFull | misskey.entities.Note
): string => {
/* /*
if (note.deletedAt) { if (note.deletedAt) {
return `(${i18n.ts.deletedNote})`; return `(${i18n.ts.deletedNote})`;
@ -22,8 +25,9 @@ export const getNoteSummary = (note: misskey.entities.Note): string => {
} }
// ファイルが添付されているとき // ファイルが添付されているとき
if ((note.files || []).length !== 0) { const files = magTransProperty(note, "attachments", "files");
const len = note.files?.length; if ((files || []).length !== 0) {
const len = files?.length;
summary += ` 📎${len !== 1 ? ` (${len})` : ""}`; summary += ` 📎${len !== 1 ? ` (${len})` : ""}`;
} }

View File

@ -9,6 +9,7 @@ import {
ref, ref,
Ref, Ref,
} from "vue"; } from "vue";
import { packed } from "magnetar-common";
export const setPageMetadata = Symbol("setPageMetadata"); export const setPageMetadata = Symbol("setPageMetadata");
export const pageMetadataProvider = Symbol("pageMetadataProvider"); export const pageMetadataProvider = Symbol("pageMetadataProvider");
@ -17,7 +18,7 @@ export type PageMetadata = {
title: string; title: string;
subtitle?: string; subtitle?: string;
icon?: string | null; icon?: string | null;
avatar?: misskey.entities.User | null; avatar?: packed.PackUserBase | misskey.entities.User | null;
userName?: misskey.entities.User | null; userName?: misskey.entities.User | null;
bg?: string; bg?: string;
}; };

View File

@ -1,6 +1,8 @@
import { ref } from "vue"; import { ref } from "vue";
import tinycolor from "tinycolor2"; import tinycolor from "tinycolor2";
import { globalEvents } from "@/events"; import { globalEvents } from "@/events";
import lightTheme from "@/themes/_light.json5";
import darkTheme from "@/themes/_dark.json5";
export type Theme = { export type Theme = {
id: string; id: string;
@ -11,10 +13,6 @@ export type Theme = {
props: Record<string, string>; props: Record<string, string>;
}; };
import lightTheme from "@/themes/_light.json5";
import darkTheme from "@/themes/_dark.json5";
import { deepClone } from "./clone";
export const themeProps = Object.keys(lightTheme.props).filter( export const themeProps = Object.keys(lightTheme.props).filter(
(key) => !key.startsWith("X") (key) => !key.startsWith("X")
); );
@ -77,7 +75,7 @@ export function applyTheme(theme: Theme, persist = true) {
const colorSchema = theme.base === "dark" ? "dark" : "light"; const colorSchema = theme.base === "dark" ? "dark" : "light";
// Deep copy // Deep copy
const _theme = deepClone(theme); const _theme = structuredClone(theme);
if (_theme.base) { if (_theme.base) {
const base = [lightTheme, darkTheme].find((x) => x.id === _theme.base); const base = [lightTheme, darkTheme].find((x) => x.id === _theme.base);

View File

@ -3,10 +3,16 @@ import * as misskey from "calckey-js";
import { stream } from "@/stream"; import { stream } from "@/stream";
import { $i } from "@/account"; import { $i } from "@/account";
import * as os from "@/os"; import * as os from "@/os";
import { endpoints, packed } from "magnetar-common";
import {
magConvertReaction,
magReactionIndex,
noteIsMag,
} from "@/scripts-mag/mag-util";
export function useNoteCapture(props: { export function useNoteCapture(props: {
rootEl: Ref<HTMLElement>; rootEl: Ref<HTMLElement>;
note: Ref<misskey.entities.Note>; note: Ref<packed.PackNoteMaybeFull | misskey.entities.Note>;
isDeletedRef: Ref<boolean>; isDeletedRef: Ref<boolean>;
}) { }) {
const note = props.note; const note = props.note;
@ -17,80 +23,188 @@ export function useNoteCapture(props: {
if (id !== note.value.id) return; if (id !== note.value.id) return;
switch (type) { if (noteIsMag(note.value)) {
case "reacted": { switch (type) {
const reaction = body.reaction; case "reacted": {
const reaction = body.reaction as string;
if (body.emoji) { if (body.emoji) {
const emojis = note.value.emojis || []; const emojis = note.value.emojis || [];
if (!emojis.includes(body.emoji)) { if (!emojis.includes(body.emoji)) {
note.value.emojis = [...emojis, body.emoji]; note.value.emojis = [
...emojis,
{
id: body.emoji.id,
shortcode: body.emoji.name,
url: body.emoji.url,
width: body.emoji.width ?? null,
height: body.emoji.height ?? null,
category: null,
},
];
}
} }
const reactionType = magConvertReaction(reaction);
const foundReaction = magReactionIndex(
note.value.reactions,
reactionType
);
const selfReact = ($i && body.userId === $i.id) || false;
if (foundReaction >= 0) {
note.value.reactions[foundReaction] = [
note.value.reactions[foundReaction][0],
note.value.reactions[foundReaction][1] + 1,
selfReact,
];
} else {
note.value.reactions.push([reactionType, 1, selfReact]);
}
break;
} }
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる case "unreacted": {
const currentCount = note.value.reactions?.[reaction] || 0; const reaction = body.reaction;
note.value.reactions[reaction] = currentCount + 1; const reactionType = magConvertReaction(reaction);
const foundReaction = magReactionIndex(
note.value.reactions,
reactionType
);
if ($i && body.userId === $i.id) { if (foundReaction >= 0) {
note.value.myReaction = reaction; const cnt = note.value.reactions[foundReaction][1];
} note.value.reactions[foundReaction] = [
break; note.value.reactions[foundReaction][0],
} cnt === 0 ? 0 : cnt - 1,
false,
];
}
case "unreacted": { break;
const reaction = body.reaction;
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
const currentCount = note.value.reactions?.[reaction] || 0;
note.value.reactions[reaction] = Math.max(0, currentCount - 1);
if ($i && body.userId === $i.id) {
note.value.myReaction = undefined;
}
break;
}
case "pollVoted": {
const choice = body.choice;
if (note.value.poll) {
const choices = [...note.value.poll.choices];
choices[choice] = {
...choices[choice],
votes: choices[choice].votes + 1,
...($i && body.userId === $i.id
? {
isVoted: true,
}
: {}),
};
note.value.poll.choices = choices;
} }
break; case "pollVoted": {
const choice = body.choice;
if (note.value.poll) {
const options = [...note.value.poll.options];
options[choice] = {
...options[choice],
votes_count: options[choice].votes_count + 1,
voted: ($i && body.userId === $i.id) || null,
};
note.value.poll.options = options;
}
break;
}
case "deleted": {
props.isDeletedRef.value = true;
break;
}
case "updated": {
const editedNote = await os.magApi(
endpoints.GetNoteById,
{
attachments: true,
context: true,
},
{ id }
);
const keys = new Set<string>();
Object.keys(editedNote)
.concat(Object.keys(note.value))
.forEach((key) => keys.add(key));
keys.forEach((key) => {
note.value[key] = editedNote[key];
});
break;
}
} }
} else {
switch (type) {
case "reacted": {
const reaction = body.reaction;
case "deleted": { if (body.emoji) {
props.isDeletedRef.value = true; const emojis = note.value.emojis || [];
break; if (!emojis.includes(body.emoji)) {
} note.value.emojis = [...emojis, body.emoji];
}
}
case "updated": { // TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
const editedNote = await os.api("notes/show", { const currentCount = note.value.reactions?.[reaction] || 0;
noteId: id,
});
const keys = new Set<string>(); note.value.reactions[reaction] = currentCount + 1;
Object.keys(editedNote)
.concat(Object.keys(note.value)) if ($i && body.userId === $i.id) {
.forEach((key) => keys.add(key)); note.value.myReaction = reaction;
keys.forEach((key) => { }
note.value[key] = editedNote[key]; break;
}); }
break;
case "unreacted": {
const reaction = body.reaction;
// TODO: reactionsプロパティがない場合ってあったっけ なければ || {} は消せる
const currentCount = note.value.reactions?.[reaction] || 0;
note.value.reactions[reaction] = Math.max(
0,
currentCount - 1
);
if ($i && body.userId === $i.id) {
note.value.myReaction = undefined;
}
break;
}
case "pollVoted": {
const choice = body.choice;
if (note.value.poll) {
const choices = [...note.value.poll.choices];
choices[choice] = {
...choices[choice],
votes: choices[choice].votes + 1,
...($i && body.userId === $i.id
? {
isVoted: true,
}
: {}),
};
note.value.poll.choices = choices;
}
break;
}
case "deleted": {
props.isDeletedRef.value = true;
break;
}
case "updated": {
const editedNote = await os.api("notes/show", {
noteId: id,
});
const keys = new Set<string>();
Object.keys(editedNote)
.concat(Object.keys(note.value))
.forEach((key) => keys.add(key));
keys.forEach((key) => {
note.value[key] = editedNote[key];
});
break;
}
} }
} }
} }

View File

@ -6,11 +6,16 @@ import { Storage } from "./pizzax";
import lightTheme from "@/themes/l-rosepinedawn.json5"; import lightTheme from "@/themes/l-rosepinedawn.json5";
import darkTheme from "@/themes/d-rosepine.json5"; import darkTheme from "@/themes/d-rosepine.json5";
export const postFormActions = []; export const postFormActions: {
export const userActions = []; title: any;
export const noteActions = []; handler: (form: any, update: any) => void;
export const noteViewInterruptors = []; }[] = [];
export const notePostInterruptors = []; export const userActions: { title: any; handler: (user: any) => void }[] = [];
export const noteActions: { title: any; handler: (note: any) => void }[] = [];
export const noteViewInterruptors: { handler: (note: any) => Promise<any> }[] =
[];
export const notePostInterruptors: { handler: (note: any) => Promise<any> }[] =
[];
const menuOptions = [ const menuOptions = [
"notifications", "notifications",

View File

@ -3,7 +3,6 @@ import { markRaw } from "vue";
import { notificationTypes } from "calckey-js"; import { notificationTypes } from "calckey-js";
import { Storage } from "../../pizzax"; import { Storage } from "../../pizzax";
import { api } from "@/os"; import { api } from "@/os";
import { deepClone } from "@/scripts/clone";
type ColumnWidget = { type ColumnWidget = {
name: string; name: string;
@ -158,7 +157,7 @@ export function swapColumn(a: Column["id"], b: Column["id"]) {
const aY = deckStore.state.layout[aX].findIndex((id) => id === a); const aY = deckStore.state.layout[aX].findIndex((id) => id === a);
const bX = deckStore.state.layout.findIndex((ids) => ids.indexOf(b) !== -1); const bX = deckStore.state.layout.findIndex((ids) => ids.indexOf(b) !== -1);
const bY = deckStore.state.layout[bX].findIndex((id) => id === b); const bY = deckStore.state.layout[bX].findIndex((id) => id === b);
const layout = deepClone(deckStore.state.layout); const layout = structuredClone(deckStore.state.layout);
layout[aX][aY] = b; layout[aX][aY] = b;
layout[bX][bY] = a; layout[bX][bY] = a;
deckStore.set("layout", layout); deckStore.set("layout", layout);
@ -166,7 +165,7 @@ export function swapColumn(a: Column["id"], b: Column["id"]) {
} }
export function swapLeftColumn(id: Column["id"]) { export function swapLeftColumn(id: Column["id"]) {
const layout = deepClone(deckStore.state.layout); const layout = structuredClone(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => { deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) { if (ids.includes(id)) {
const left = deckStore.state.layout[i - 1]; const left = deckStore.state.layout[i - 1];
@ -182,7 +181,7 @@ export function swapLeftColumn(id: Column["id"]) {
} }
export function swapRightColumn(id: Column["id"]) { export function swapRightColumn(id: Column["id"]) {
const layout = deepClone(deckStore.state.layout); const layout = structuredClone(deckStore.state.layout);
deckStore.state.layout.some((ids, i) => { deckStore.state.layout.some((ids, i) => {
if (ids.includes(id)) { if (ids.includes(id)) {
const right = deckStore.state.layout[i + 1]; const right = deckStore.state.layout[i + 1];
@ -198,11 +197,11 @@ export function swapRightColumn(id: Column["id"]) {
} }
export function swapUpColumn(id: Column["id"]) { export function swapUpColumn(id: Column["id"]) {
const layout = deepClone(deckStore.state.layout); const layout = structuredClone(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex((ids) => const idsIndex = deckStore.state.layout.findIndex((ids) =>
ids.includes(id) ids.includes(id)
); );
const ids = deepClone(deckStore.state.layout[idsIndex]); const ids = structuredClone(deckStore.state.layout[idsIndex]);
ids.some((x, i) => { ids.some((x, i) => {
if (x === id) { if (x === id) {
const up = ids[i - 1]; const up = ids[i - 1];
@ -220,11 +219,11 @@ export function swapUpColumn(id: Column["id"]) {
} }
export function swapDownColumn(id: Column["id"]) { export function swapDownColumn(id: Column["id"]) {
const layout = deepClone(deckStore.state.layout); const layout = structuredClone(deckStore.state.layout);
const idsIndex = deckStore.state.layout.findIndex((ids) => const idsIndex = deckStore.state.layout.findIndex((ids) =>
ids.includes(id) ids.includes(id)
); );
const ids = deepClone(deckStore.state.layout[idsIndex]); const ids = structuredClone(deckStore.state.layout[idsIndex]);
ids.some((x, i) => { ids.some((x, i) => {
if (x === id) { if (x === id) {
const down = ids[i + 1]; const down = ids[i + 1];
@ -242,7 +241,7 @@ export function swapDownColumn(id: Column["id"]) {
} }
export function stackLeftColumn(id: Column["id"]) { export function stackLeftColumn(id: Column["id"]) {
let layout = deepClone(deckStore.state.layout); let layout = structuredClone(deckStore.state.layout);
const i = deckStore.state.layout.findIndex((ids) => ids.includes(id)); const i = deckStore.state.layout.findIndex((ids) => ids.includes(id));
layout = layout.map((ids) => ids.filter((_id) => _id !== id)); layout = layout.map((ids) => ids.filter((_id) => _id !== id));
layout[i - 1].push(id); layout[i - 1].push(id);
@ -252,7 +251,7 @@ export function stackLeftColumn(id: Column["id"]) {
} }
export function popRightColumn(id: Column["id"]) { export function popRightColumn(id: Column["id"]) {
let layout = deepClone(deckStore.state.layout); let layout = structuredClone(deckStore.state.layout);
const i = deckStore.state.layout.findIndex((ids) => ids.includes(id)); const i = deckStore.state.layout.findIndex((ids) => ids.includes(id));
const affected = layout[i]; const affected = layout[i];
layout = layout.map((ids) => ids.filter((_id) => _id !== id)); layout = layout.map((ids) => ids.filter((_id) => _id !== id));
@ -260,7 +259,7 @@ export function popRightColumn(id: Column["id"]) {
layout = layout.filter((ids) => ids.length > 0); layout = layout.filter((ids) => ids.length > 0);
deckStore.set("layout", layout); deckStore.set("layout", layout);
const columns = deepClone(deckStore.state.columns); const columns = structuredClone(deckStore.state.columns);
for (const column of columns) { for (const column of columns) {
if (affected.includes(column.id)) { if (affected.includes(column.id)) {
column.active = true; column.active = true;
@ -272,9 +271,9 @@ export function popRightColumn(id: Column["id"]) {
} }
export function addColumnWidget(id: Column["id"], widget: ColumnWidget) { export function addColumnWidget(id: Column["id"], widget: ColumnWidget) {
const columns = deepClone(deckStore.state.columns); const columns = structuredClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id); const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id);
const column = deepClone(deckStore.state.columns[columnIndex]); const column = structuredClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
if (column.widgets == null) column.widgets = []; if (column.widgets == null) column.widgets = [];
column.widgets.unshift(widget); column.widgets.unshift(widget);
@ -284,9 +283,9 @@ export function addColumnWidget(id: Column["id"], widget: ColumnWidget) {
} }
export function removeColumnWidget(id: Column["id"], widget: ColumnWidget) { export function removeColumnWidget(id: Column["id"], widget: ColumnWidget) {
const columns = deepClone(deckStore.state.columns); const columns = structuredClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id); const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id);
const column = deepClone(deckStore.state.columns[columnIndex]); const column = structuredClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = column.widgets.filter((w) => w.id !== widget.id); column.widgets = column.widgets.filter((w) => w.id !== widget.id);
columns[columnIndex] = column; columns[columnIndex] = column;
@ -295,9 +294,9 @@ export function removeColumnWidget(id: Column["id"], widget: ColumnWidget) {
} }
export function setColumnWidgets(id: Column["id"], widgets: ColumnWidget[]) { export function setColumnWidgets(id: Column["id"], widgets: ColumnWidget[]) {
const columns = deepClone(deckStore.state.columns); const columns = structuredClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id); const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id);
const column = deepClone(deckStore.state.columns[columnIndex]); const column = structuredClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = widgets; column.widgets = widgets;
columns[columnIndex] = column; columns[columnIndex] = column;
@ -310,9 +309,9 @@ export function updateColumnWidget(
widgetId: string, widgetId: string,
widgetData: any widgetData: any
) { ) {
const columns = deepClone(deckStore.state.columns); const columns = structuredClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id); const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id);
const column = deepClone(deckStore.state.columns[columnIndex]); const column = structuredClone(deckStore.state.columns[columnIndex]);
if (column == null) return; if (column == null) return;
column.widgets = column.widgets.map((w) => column.widgets = column.widgets.map((w) =>
w.id === widgetId w.id === widgetId
@ -328,9 +327,9 @@ export function updateColumnWidget(
} }
export function updateColumn(id: Column["id"], column: Partial<Column>) { export function updateColumn(id: Column["id"], column: Partial<Column>) {
const columns = deepClone(deckStore.state.columns); const columns = structuredClone(deckStore.state.columns);
const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id); const columnIndex = deckStore.state.columns.findIndex((c) => c.id === id);
const currentColumn = deepClone(deckStore.state.columns[columnIndex]); const currentColumn = structuredClone(deckStore.state.columns[columnIndex]);
if (currentColumn == null) return; if (currentColumn == null) return;
for (const [k, v] of Object.entries(column)) { for (const [k, v] of Object.entries(column)) {
currentColumn[k] = v; currentColumn[k] = v;

View File

@ -123,20 +123,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, reactive, ref } from "vue"; import { onUnmounted, reactive } from "vue";
import { import { useWidgetPropsManager, Widget, WidgetComponentExpose } from "./widget";
useWidgetPropsManager,
Widget,
WidgetComponentEmits,
WidgetComponentExpose,
WidgetComponentProps,
} from "./widget";
import { GetFormResultType } from "@/scripts/form"; import { GetFormResultType } from "@/scripts/form";
import { stream } from "@/stream"; import { stream } from "@/stream";
import number from "@/filters/number"; import number from "@/filters/number";
import * as sound from "@/scripts/sound"; import * as sound from "@/scripts/sound";
import * as os from "@/os";
import { deepClone } from "@/scripts/clone";
const name = "jobQueue"; const name = "jobQueue";
@ -185,12 +177,12 @@ const prev = reactive({} as typeof current);
const jammedSound = sound.setVolume(sound.getAudio("syuilo/queue-jammed"), 1); const jammedSound = sound.setVolume(sound.getAudio("syuilo/queue-jammed"), 1);
for (const domain of ["inbox", "deliver"]) { for (const domain of ["inbox", "deliver"]) {
prev[domain] = deepClone(current[domain]); prev[domain] = structuredClone(current[domain]);
} }
const onStats = (stats) => { const onStats = (stats) => {
for (const domain of ["inbox", "deliver"]) { for (const domain of ["inbox", "deliver"]) {
prev[domain] = deepClone(current[domain]); prev[domain] = structuredClone(current[domain]);
current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick; current[domain].activeSincePrevTick = stats[domain].activeSincePrevTick;
current[domain].active = stats[domain].active; current[domain].active = stats[domain].active;
current[domain].waiting = stats[domain].waiting; current[domain].waiting = stats[domain].waiting;

View File

@ -1,8 +1,7 @@
import { reactive, watch } from "vue"; import { reactive, toRaw, watch } from "vue";
import { throttle } from "throttle-debounce"; import { throttle } from "throttle-debounce";
import { Form, GetFormResultType } from "@/scripts/form"; import { Form, GetFormResultType } from "@/scripts/form";
import * as os from "@/os"; import * as os from "@/os";
import { deepClone } from "@/scripts/clone";
export type Widget<P extends Record<string, unknown>> = { export type Widget<P extends Record<string, unknown>> = {
id: string; id: string;
@ -36,7 +35,7 @@ export const useWidgetPropsManager = <
configure: () => void; configure: () => void;
} => { } => {
const widgetProps = reactive( const widgetProps = reactive(
props.widget ? deepClone(props.widget.data) : {} props.widget ? structuredClone(toRaw(props.widget.data)) : {}
); );
const mergeProps = () => { const mergeProps = () => {
@ -59,7 +58,7 @@ export const useWidgetPropsManager = <
}); });
const configure = async () => { const configure = async () => {
const form = deepClone(propsDef); const form = structuredClone(toRaw(propsDef));
for (const item of Object.keys(form)) { for (const item of Object.keys(form)) {
form[item].default = widgetProps[item]; form[item].default = widgetProps[item];
} }

View File

@ -36,7 +36,8 @@
"esnext", "esnext",
"dom" "dom"
], ],
"jsx": "preserve" "jsx": "preserve",
"jsxImportSource": "vue"
}, },
"compileOnSave": false, "compileOnSave": false,
"include": [ "include": [

View File

@ -1,7 +1,8 @@
{ {
"name": "magnetar-common", "name": "magnetar-common",
"version": "0.0.1", "version": "0.0.1",
"main": "index.js", "main": "./built/index.js",
"types": "./built/index.d.ts",
"scripts": { "scripts": {
"build": "tsc" "build": "tsc"
}, },

View File

@ -0,0 +1,126 @@
export type Method = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
export interface BackendApiEndpoint<
M extends Method,
PP extends string[],
T,
R
> {
method: M;
endpoint: string;
pathParams: PP;
request?: T;
response?: R;
}
function nestedUrlSearchParams(data: any, topLevel: boolean = true): string {
switch (typeof data) {
case "string":
case "bigint":
case "boolean":
case "number":
case "symbol":
if (topLevel) return encodeURIComponent(data.toString()) + "=";
return data.toString();
case "object":
if (data === null) return "null";
if (Array.isArray(data))
return data
.map((d) => nestedUrlSearchParams(d, true))
.map(encodeURIComponent)
.join("&");
const inner = Object.entries(data).map(([k, v]) => [
k,
nestedUrlSearchParams(v, false),
]);
return new URLSearchParams(inner).toString();
default:
return "";
}
}
export type MagApiErrorCode = "Client:GenericApiError" | string;
export interface MagApiError {
status: number;
code: MagApiErrorCode;
message: string;
}
export class MagApiClient {
private readonly baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
async call<
T extends BackendApiEndpoint<
T["method"] & Method,
T["pathParams"] & string[],
T["request"],
T["response"]
>
>(
{ endpoint, method }: T,
data: T["request"],
pathParams: {
[K in keyof T["pathParams"] as T["pathParams"][K] & string]:
| string
| number;
},
token?: string | null | undefined
): Promise<T["response"]> {
type Response = T["response"];
const authorizationToken = token ?? undefined;
const authorization = authorizationToken
? `Bearer ${authorizationToken}`
: undefined;
for (const name in pathParams) {
endpoint = endpoint.replace(`:${name}`, `${pathParams[name]}`);
}
const baseUrl = this.baseUrl.replace(/\/+$/, "");
let url = `${baseUrl}${endpoint}`;
if (method === "GET") {
const query = nestedUrlSearchParams(data as any);
if (query) {
url += `?${query}`;
}
}
return await fetch(url, {
method,
body: method !== "GET" ? JSON.stringify(data) : undefined,
credentials: "omit",
cache: "no-cache",
headers: authorization ? { authorization } : {},
})
.then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
return body as Response;
} else if (res.status === 204) {
return null as any as Response;
} else {
throw body as MagApiError;
}
})
.catch((e) => {
throw {
status: -1,
code: "Client:GenericApiError",
message: e,
} as MagApiError;
});
}
}

View File

@ -0,0 +1,5 @@
export { GetNoteById } from "./types/endpoints/GetNoteById";
export { GetTimeline } from "./types/endpoints/GetTimeline";
export { GetUserById } from "./types/endpoints/GetUserById";
export { GetUserByAcct } from "./types/endpoints/GetUserByAcct";
export { GetUserSelf } from "./types/endpoints/GetUserSelf";

View File

@ -0,0 +1,28 @@
import {
BackendApiEndpoint,
MagApiClient,
MagApiError,
MagApiErrorCode,
Method,
} from "./be-api";
import {
feEndpoints,
FrontendApiEndpoint,
FrontendApiEndpoints,
} from "./fe-api";
export * as types from "./types";
export * as packed from "./packed";
export * as endpoints from "./endpoints";
export {
Method,
BackendApiEndpoint,
MagApiError,
MagApiClient,
MagApiErrorCode,
feEndpoints,
FrontendApiEndpoint,
FrontendApiEndpoints,
};

View File

@ -0,0 +1,16 @@
export { PackDriveFileBase } from "./types/packed/PackDriveFileBase";
export { PackDriveFileFull } from "./types/packed/PackDriveFileFull";
export { PackDriveFileWithFolder } from "./types/packed/PackDriveFileWithFolder";
export { PackDriveFileWithUser } from "./types/packed/PackDriveFileWithUser";
export { PackDriveFolderBase } from "./types/packed/PackDriveFolderBase";
export { PackEmojiBase } from "./types/packed/PackEmojiBase";
export { PackDriveFolderWithParent } from "./types/packed/PackDriveFolderWithParent";
export { PackNoteBase } from "./types/packed/PackNoteBase";
export { PackNoteMaybeFull } from "./types/packed/PackNoteMaybeFull";
export { PackPollBase } from "./types/packed/PackPollBase";
export { PackNoteMaybeAttachments } from "./types/packed/PackNoteMaybeAttachments";
export { PackSecurityKeyBase } from "./types/packed/PackSecurityKeyBase";
export { PackUserBase } from "./types/packed/PackUserBase";
export { PackUserMaybeAll } from "./types/packed/PackUserMaybeAll";
export { PackUserSelf } from "./types/packed/PackUserSelf";
export { PackUserSelfMaybeAll } from "./types/packed/PackUserSelfMaybeAll";

View File

@ -0,0 +1,40 @@
export { EmojiContext } from "./types/EmojiContext";
export { FollowVisibility } from "./types/FollowVisibility";
export { Id } from "./types/Id";
export { NotificationSettings } from "./types/NotificationSettings";
export { EmojiBase } from "./types/EmojiBase";
export { MmXml } from "./types/MmXml";
export { NotificationType } from "./types/NotificationType";
export { NoteBase } from "./types/NoteBase";
export { NoteAttachmentExt } from "./types/NoteAttachmentExt";
export { NoteDetailExt } from "./types/NoteDetailExt";
export { NoteListFilter } from "./types/NoteListFilter";
export { NoteSelfContextExt } from "./types/NoteSelfContextExt";
export { NoteVisibility } from "./types/NoteVisibility";
export { PollBase } from "./types/PollBase";
export { PollChoice } from "./types/PollChoice";
export { Reaction } from "./types/Reaction";
export { ReactionPair } from "./types/ReactionPair";
export { AvatarDecoration } from "./types/AvatarDecoration";
export { ProfileField } from "./types/ProfileField";
export { SecurityKeyBase } from "./types/SecurityKeyBase";
export { SpeechTransform } from "./types/SpeechTransform";
export { UserAuthOverviewExt } from "./types/UserAuthOverviewExt";
export { UserBase } from "./types/UserBase";
export { UserDetailExt } from "./types/UserDetailExt";
export { UserProfileExt } from "./types/UserProfileExt";
export { UserProfilePinsEx } from "./types/UserProfilePinsEx";
export { UserSecretsExt } from "./types/UserSecretsExt";
export { UserRelationExt } from "./types/UserRelationExt";
export { UserSelfExt } from "./types/UserSelfExt";
export { NoteByIdReq } from "./types/NoteByIdReq";
export { GetTimelineReq } from "./types/GetTimelineReq";
export { UserByIdReq } from "./types/UserByIdReq";
export { UserSelfReq } from "./types/UserSelfReq";
export { DriveFileBase } from "./types/DriveFileBase";
export { DriveFileFolderExt } from "./types/DriveFileFolderExt";
export { DriveFileUserExt } from "./types/DriveFileUserExt";
export { DriveFolderBase } from "./types/DriveFolderBase";
export { DriveFolderParentExt } from "./types/DriveFolderParentExt";
export { ImageMeta } from "./types/ImageMeta";
export { InstanceTicker } from "./types/InstanceTicker";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AvatarDecoration = "None" | "CatEars";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ImageMeta } from "./ImageMeta";
export interface DriveFileBase { name: string, created_at: string, size: number, hash: string | null, mime_type: string, media_metadata: ImageMeta, url: string | null, source_url: string, thumbnail_url: string | null, blurhash: string | null, sensitive: boolean, comment: string | null, folder_id: string | null, user_id: string | null, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackDriveFolderBase } from "./packed/PackDriveFolderBase";
export interface DriveFileFolderExt { folder: PackDriveFolderBase, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackUserBase } from "./packed/PackUserBase";
export interface DriveFileUserExt { user: PackUserBase, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface DriveFolderBase { name: string, created_at: string, comment: string | null, file_count: number, folder_count: number, parent_id: string | null, user_id: string, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DriveFolderBase } from "./DriveFolderBase";
export interface DriveFolderParentExt { folder: DriveFolderBase, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface EmojiBase { shortcode: string, url: string, category: string | null, width: number | null, height: number | null, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackEmojiBase } from "./packed/PackEmojiBase";
export type EmojiContext = Array<PackEmojiBase>;

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type FollowVisibility = "Public" | "Followers" | "Private";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NoteListFilter } from "./NoteListFilter";
export interface GetTimelineReq { limit?: bigint, filter?: NoteListFilter, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface Id { id: string, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface ImageMeta { width: number | null, height: number | null, orientation: number | null, color: string | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface InstanceTicker { name: string | null, software_name: string | null, software_version: string | null, icon_url: string | null, favicon_url: string | null, theme_color: string | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type MmXml = string;

View File

@ -0,0 +1,5 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackDriveFileBase } from "./packed/PackDriveFileBase";
import type { PackPollBase } from "./packed/PackPollBase";
export interface NoteAttachmentExt { poll: PackPollBase | null, attachments: Array<PackDriveFileBase>, }

View File

@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { EmojiContext } from "./EmojiContext";
import type { MmXml } from "./MmXml";
import type { NoteVisibility } from "./NoteVisibility";
import type { PackUserBase } from "./packed/PackUserBase";
import type { ReactionPair } from "./ReactionPair";
export interface NoteBase { created_at: string, updated_at: string | null, cw: string | null, cw_mm: MmXml | null, uri: string | null, url: string | null, text: string, text_mm: MmXml | null, visibility: NoteVisibility, user: PackUserBase, parent_note_id: string | null, renoted_note_id: string | null, reply_count: number, renote_count: number, mentions: Array<string>, visible_user_ids: Array<string> | null, hashtags: Array<string>, reactions: Array<ReactionPair>, local_only: boolean, has_poll: boolean, file_ids: Array<string>, emojis: EmojiContext, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface NoteByIdReq { context?: boolean, attachments?: boolean, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PackNoteMaybeAttachments } from "./packed/PackNoteMaybeAttachments";
export interface NoteDetailExt { parent_note: PackNoteMaybeAttachments | null, renoted_note: PackNoteMaybeAttachments | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface NoteListFilter { show_renotes: boolean | null, show_replies: boolean | null, show_files_only: boolean | null, uncwed_sensitive: boolean | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface NoteSelfContextExt { self_renote_count: number | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NoteVisibility = "Public" | "Home" | "Followers" | "Direct";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { NotificationType } from "./NotificationType";
export interface NotificationSettings { enabled: Array<NotificationType>, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type NotificationType = "Follow" | "Mention" | "Reply" | "Renote" | "Quote" | "Reaction" | "PollVote" | "PollEnded" | "FollowRequest" | "FollowRequestAccepted" | "App";

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { PollChoice } from "./PollChoice";
export interface PollBase { expires_at: string | null, expired: boolean, multiple_choice: boolean, options: Array<PollChoice>, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface PollChoice { title: string, votes_count: number, voted: boolean | null, }

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { MmXml } from "./MmXml";
export interface ProfileField { name: string, value: string, value_mm: MmXml | null, verified_at: string | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type Reaction = string | { name: string, host: string | null, url: string, } | { raw: string, };

View File

@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Reaction } from "./Reaction";
export type ReactionPair = [Reaction, number] | [Reaction, number, boolean];

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface SecurityKeyBase { name: string, last_used_at: string | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type SpeechTransform = "None" | "Cat";

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface UserAuthOverviewExt { has_two_factor_enabled: boolean, has_passwordless_login: boolean, has_security_keys: boolean, }

View File

@ -0,0 +1,8 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AvatarDecoration } from "./AvatarDecoration";
import type { EmojiContext } from "./EmojiContext";
import type { InstanceTicker } from "./InstanceTicker";
import type { MmXml } from "./MmXml";
import type { SpeechTransform } from "./SpeechTransform";
export interface UserBase { acct: string, username: string, display_name: string, display_name_mm: MmXml | null, host: string | null, speech_transform: SpeechTransform, created_at: string, avatar_url: string, avatar_blurhash: string | null, avatar_decoration: AvatarDecoration, is_admin: boolean, is_moderator: boolean, is_bot: boolean, emojis: EmojiContext, instance: InstanceTicker | null, }

View File

@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export interface UserByIdReq { profile?: boolean, pins?: boolean, detail?: boolean, relation?: boolean, auth?: boolean, }

Some files were not shown because too many files have changed in this diff Show More