Merge branch 'develop' of codeberg.org:calckey/calckey into develop

This commit is contained in:
ThatOneCalculator 2023-03-17 09:31:47 -07:00
commit 3066d6079a
No known key found for this signature in database
GPG key ID: 8703CACD01000000
22 changed files with 543 additions and 182 deletions

View file

@ -835,7 +835,7 @@ muteThread: "Mute thread"
unmuteThread: "Unmute thread" unmuteThread: "Unmute thread"
ffVisibility: "Follows/Followers Visibility" ffVisibility: "Follows/Followers Visibility"
ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you." ffVisibilityDescription: "Allows you to configure who can see who you follow and who follows you."
continueThread: "View thread continuation" continueThread: "Continue thread"
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?" deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
incorrectPassword: "Incorrect password." incorrectPassword: "Incorrect password."
voteConfirm: "Confirm your vote for \"{choice}\"?" voteConfirm: "Confirm your vote for \"{choice}\"?"

View file

@ -359,7 +359,7 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = (await client.getBookmarks(ctx.query as any)) as any; const data = (await client.getBookmarks(limitToInt(ctx.query as any))) as any;
let resp = data.data; let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) { for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
@ -383,7 +383,7 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getFavourites(ctx.query as any); const data = await client.getFavourites(limitToInt(ctx.query as any));
let resp = data.data; let resp = data.data;
for (let statIdx = 0; statIdx < resp.length; statIdx++) { for (let statIdx = 0; statIdx < resp.length; statIdx++) {
resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId); resp[statIdx].id = convertId(resp[statIdx].id, IdType.MastodonId);
@ -407,7 +407,7 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getMutes(ctx.query as any); const data = await client.getMutes(limitToInt(ctx.query as any));
let resp = data.data; let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);
@ -425,7 +425,7 @@ export function apiAccountMastodon(router: Router): void {
const accessTokens = ctx.headers.authorization; const accessTokens = ctx.headers.authorization;
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const data = await client.getBlocks(ctx.query as any); const data = await client.getBlocks(limitToInt(ctx.query as any));
let resp = data.data; let resp = data.data;
for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) { for (let acctIdx = 0; acctIdx < resp.length; acctIdx++) {
resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId); resp[acctIdx].id = convertId(resp[acctIdx].id, IdType.MastodonId);

View file

@ -4,6 +4,8 @@ import { emojiRegexAtStartToEnd } from "@/misc/emoji-regex.js";
import axios from "axios"; import axios from "axios";
import querystring from 'node:querystring' import querystring from 'node:querystring'
import qs from 'qs' import qs from 'qs'
import { limitToInt } from "./timeline.js";
function normalizeQuery(data: any) { function normalizeQuery(data: any) {
const str = querystring.stringify(data); const str = querystring.stringify(data);
return qs.parse(str); return qs.parse(str);
@ -101,7 +103,7 @@ export function apiStatusMastodon(router: Router): void {
const client = getClient(BASE_URL, accessTokens); const client = getClient(BASE_URL, accessTokens);
try { try {
const id = ctx.params.id; const id = ctx.params.id;
const data = await client.getStatusContext(id, ctx.query as any); const data = await client.getStatusContext(id, limitToInt(ctx.query as any));
const status = await client.getStatus(id); const status = await client.getStatus(id);
const reactionsAxios = await axios.get( const reactionsAxios = await axios.get(
`${BASE_URL}/api/notes/reactions?noteId=${id}`, `${BASE_URL}/api/notes/reactions?noteId=${id}`,

View file

@ -15,13 +15,16 @@ export function limitToInt(q: ParsedUrlQuery) {
} }
export function argsToBools(q: ParsedUrlQuery) { export function argsToBools(q: ParsedUrlQuery) {
// Values taken from https://docs.joinmastodon.org/client/intro/#boolean
const toBoolean = (value: string) => !['0', 'f', 'F', 'false', 'FALSE', 'off', 'OFF'].includes(value);
let object: any = q; let object: any = q;
if (q.only_media) if (q.only_media)
if (typeof q.only_media === "string") if (typeof q.only_media === "string")
object.only_media = q.only_media.toLowerCase() === "true"; object.only_media = toBoolean(q.only_media);
if (q.exclude_replies) if (q.exclude_replies)
if (typeof q.exclude_replies === "string") if (typeof q.exclude_replies === "string")
object.exclude_replies = q.exclude_replies.toLowerCase() === "true"; object.exclude_replies = toBoolean(q.exclude_replies);
return q; return q;
} }

View file

@ -1,5 +1,5 @@
<template> <template>
<button class="nrvgflfu _button" @click.stop.prevent="toggle"> <button class="nrvgflfu _button" @click.stop="toggle">
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b> <b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
<span v-if="!modelValue">{{ label }}</span> <span v-if="!modelValue">{{ label }}</span>
</button> </button>
@ -36,6 +36,8 @@ const toggle = () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.nrvgflfu { .nrvgflfu {
position: relative;
z-index: 2;
display: inline-block; display: inline-block;
padding: 4px 8px; padding: 4px 8px;
font-size: 0.8em; font-size: 0.8em;

View file

@ -2,7 +2,7 @@
<div class="hoawjimk"> <div class="hoawjimk">
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/> <XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ dmWidth: inDm }"> <div v-if="mediaList.filter(media => previewable(media)).length > 0" class="gird-container" :class="{ dmWidth: inDm }">
<div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop.prevent> <div ref="gallery" :data-count="mediaList.filter(media => previewable(media)).length" @click.stop>
<template v-for="media in mediaList.filter(media => previewable(media))"> <template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/> <XVideo v-if="media.type.startsWith('video')" :key="media.id" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/> <XImage v-else-if="media.type.startsWith('image')" :key="media.id" class="image" :data-id="media.id" :image="media" :raw="raw"/>

View file

@ -1,12 +1,12 @@
<template> <template>
<MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }"> <MkA v-if="url.startsWith('/')" v-user-preview="canonical" class="akbvjaqn" :class="{ isMe }" :to="url" :style="{ background: bgCss }" @click.stop>
<img class="icon" :src="`/avatar/@${username}@${host}`" alt=""> <img class="icon" :src="`/avatar/@${username}@${host}`" alt="">
<span class="main"> <span class="main">
<span class="username">@{{ username }}</span> <span class="username">@{{ username }}</span>
<span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span> <span v-if="(host != localHost) || $store.state.showFullAcct" class="host">@{{ toUnicode(host) }}</span>
</span> </span>
</MkA> </MkA>
<a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }"> <a v-else class="akbvjaqn" :href="url" target="_blank" rel="noopener" :style="{ background: bgCss }" @click.stop>
<span class="main"> <span class="main">
<span class="username">@{{ username }}</span> <span class="username">@{{ username }}</span>
<span class="host">@{{ toUnicode(host) }}</span> <span class="host">@{{ toUnicode(host) }}</span>
@ -42,8 +42,13 @@ const bgCss = bg.toRgbString();
<style lang="scss" scoped> <style lang="scss" scoped>
.akbvjaqn { .akbvjaqn {
display: inline-block; display: inline-block;
padding: 4px 8px 4px 4px; padding: 2px 8px 2px 2px;
margin-block: 2px;
border-radius: 999px; border-radius: 999px;
max-width: 100%;
white-space: nowrap;
overflow: clip;
text-overflow: ellipsis;
color: var(--mention); color: var(--mention);
&.isMe { &.isMe {

View file

@ -25,7 +25,7 @@
</template> </template>
</I18n> </I18n>
<div class="info"> <div class="info">
<button ref="renoteTime" class="_button time" @click="showRenoteMenu()"> <button ref="renoteTime" class="_button time" @click.stop="showRenoteMenu()">
<i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i> <i v-if="isMyRenote" class="ph-dots-three-outline ph-bold ph-lg dropdownIcon"></i>
<MkTime :time="note.createdAt"/> <MkTime :time="note.createdAt"/>
</button> </button>
@ -33,24 +33,24 @@
</div> </div>
</div> </div>
</div> </div>
<article class="article" @contextmenu.stop="onContextmenu" @click.self="router.push(notePage(appearNote))"> <article class="article" @contextmenu.stop="onContextmenu" @click="router.push(notePage(appearNote))">
<div class="main" @click.self="router.push(notePage(appearNote))"> <div class="main">
<div class="header-container"> <div class="header-container">
<MkAvatar class="avatar" :user="appearNote.user"/> <MkAvatar class="avatar" :user="appearNote.user"/>
<XNoteHeader class="header" :note="appearNote" :mini="true"/> <XNoteHeader class="header" :note="appearNote" :mini="true"/>
</div> </div>
<div class="body"> <div class="body">
<p v-if="appearNote.cw != null" class="cw"> <p v-if="appearNote.cw != null" class="cw">
<Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm v-if="appearNote.cw != ''" class="text" :text="appearNote.cw" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" @click.stop/>
<XCwButton v-model="showContent" :note="appearNote"/> <XCwButton v-model="showContent" :note="appearNote"/>
</p> </p>
<div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }"> <div v-show="appearNote.cw == null || showContent" class="content" :class="{ collapsed, isLong }">
<div class="text" @click.self="router.push(notePage(appearNote))"> <div class="text">
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis" @click.stop/>
<!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> --> <!-- <a v-if="appearNote.renote != null" class="rp">RN:</a> -->
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
<div v-else class="translated"> <div v-else class="translated" @click.stop>
<b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b> <b>{{ i18n.t('translatedFrom', { x: translation.sourceLang }) }}: </b>
<Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm :text="translation.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
</div> </div>
@ -61,36 +61,17 @@
</div> </div>
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> <XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
<button v-if="isLong && collapsed" class="fade _button" @click.stop.prevent="collapsed = false"> <button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
<span>{{ i18n.ts.showMore }}</span> <span>{{ i18n.ts.showMore }}</span>
</button> </button>
<button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop.prevent="collapsed = true"> <button v-else-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
<span>{{ i18n.ts.showLess }}</span> <span>{{ i18n.ts.showLess }}</span>
</button> </button>
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`" @click.stop><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
<footer class="footer"> <MkNoteFooter :note="appearNote"></MkNoteFooter>
<XReactionsViewer ref="reactionsViewer" :note="appearNote"/>
<button v-tooltip.noDelay.bottom="i18n.ts.reply" class="button _button" @click="reply()">
<template v-if="appearNote.reply"><i class="ph-arrow-u-up-left ph-bold ph-lg"></i></template>
<template v-else><i class="ph-arrow-bend-up-left ph-bold ph-lg"></i></template>
<p v-if="appearNote.repliesCount > 0" class="count">{{ appearNote.repliesCount }}</p>
</button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" 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="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<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> </div>
</article> </article>
</div> </div>
@ -113,15 +94,12 @@ import type * as misskey from 'calckey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';
import XNoteHeader from '@/components/MkNoteHeader.vue'; import XNoteHeader from '@/components/MkNoteHeader.vue';
import XNoteSimple from '@/components/MkNoteSimple.vue'; import XNoteSimple from '@/components/MkNoteSimple.vue';
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
import XMediaList from '@/components/MkMediaList.vue'; import XMediaList from '@/components/MkMediaList.vue';
import XCwButton from '@/components/MkCwButton.vue'; import XCwButton from '@/components/MkCwButton.vue';
import MkNoteFooter from '@/components/MkNoteFooter.vue';
import XPoll from '@/components/MkPoll.vue'; import XPoll from '@/components/MkPoll.vue';
import XStarButton from '@/components/MkStarButton.vue';
import XRenoteButton from '@/components/MkRenoteButton.vue'; import XRenoteButton from '@/components/MkRenoteButton.vue';
import XQuoteButton from '@/components/MkQuoteButton.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue'; import MkUrlPreview from '@/components/MkUrlPreview.vue';
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
import MkVisibility from '@/components/MkVisibility.vue'; import MkVisibility from '@/components/MkVisibility.vue';
import { pleaseLogin } from '@/scripts/please-login'; import { pleaseLogin } from '@/scripts/please-login';
import { focusPrev, focusNext } from '@/scripts/focus'; import { focusPrev, focusNext } from '@/scripts/focus';
@ -170,7 +148,6 @@ const isRenote = (
const el = ref<HTMLElement>(); const el = ref<HTMLElement>();
const menuButton = ref<HTMLElement>(); const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>(); const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const renoteTime = ref<HTMLElement>(); const renoteTime = ref<HTMLElement>();
const reactButton = ref<HTMLElement>(); const reactButton = ref<HTMLElement>();
@ -187,7 +164,6 @@ const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
const translation = ref(null); const translation = ref(null);
const translating = ref(false); const translating = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null; const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)).slice(0, 5) : null;
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@ -229,14 +205,6 @@ function react(viaKeyboard = false): void {
}); });
} }
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); const currentClipPage = inject<Ref<misskey.entities.Clip> | null>('currentClipPage', null);
function onContextmenu(ev: MouseEvent): void { function onContextmenu(ev: MouseEvent): void {
@ -342,19 +310,23 @@ function readPromo() {
} }
} }
&:hover > .article > .main > .footer > .button { & > .article > .main {
opacity: 1; &:hover, &:focus-within {
:deep(.footer .button) {
opacity: 1;
}
}
} }
> .reply-to { > .reply-to {
& + .note-context { & + .note-context {
.line::before { .line::before {
content: ""; content: "";
display: block; display: block;
margin-bottom: -10px; margin-bottom: -10px;
width: 2px; margin-top: 16px;
background-color: var(--divider); border-left: 2px solid var(--divider);
margin-inline: auto; margin-left: calc((var(--avatarSize) / 2) - 1px);
} }
} }
} }
@ -477,7 +449,6 @@ function readPromo() {
> .body { > .body {
margin-top: .7em; margin-top: .7em;
overflow: hidden;
> .cw { > .cw {
cursor: default; cursor: default;
@ -585,6 +556,10 @@ function readPromo() {
padding: 16px; padding: 16px;
border: solid 1px var(--renote); border: solid 1px var(--renote);
border-radius: 8px; border-radius: 8px;
transition: background .2s;
&:hover, &:focus-within {
background-color: var(--panelHighlight);
}
} }
} }
} }
@ -594,36 +569,6 @@ function readPromo() {
font-size: 80%; font-size: 80%;
} }
} }
> .footer {
display: flex;
flex-wrap: wrap;
> .button {
margin: 0;
padding: 8px;
opacity: 0.7;
flex-grow: 1;
max-width: 3.5em;
width: max-content;
min-width: max-content;
&:first-of-type {
margin-left: -.5em;
}
&:hover {
color: var(--fgHighlighted);
}
> .count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
&.reacted {
color: var(--accent);
}
}
}
} }
} }

View file

@ -53,7 +53,7 @@
<XCwButton v-model="showContent" :note="appearNote"/> <XCwButton v-model="showContent" :note="appearNote"/>
</p> </p>
<div v-show="appearNote.cw == null || showContent" class="content"> <div v-show="appearNote.cw == null || showContent" class="content">
<div class="text" @click.self="router.push(notePage(appearNote))"> <div class="text">
<Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/> <Mfm v-if="appearNote.text" :text="appearNote.text" :author="appearNote.user" :i="$i" :custom-emojis="appearNote.emojis"/>
<div v-if="translating || translation" class="translation"> <div v-if="translating || translation" class="translation">
<MkLoading v-if="translating" mini/> <MkLoading v-if="translating" mini/>
@ -68,7 +68,7 @@
</div> </div>
<XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/> <XPoll v-if="appearNote.poll" ref="pollViewer" :note="appearNote" class="poll"/>
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/> <MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="true" class="url-preview"/>
<div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote"/></div> <div v-if="appearNote.renote" class="renote"><XNoteSimple :note="appearNote.renote" @click.stop="router.push(notePage(appearNote.renote))"/></div>
</div> </div>
<MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA> <MkA v-if="appearNote.channel && !inChannel" class="channel" :to="`/channels/${appearNote.channel.id}`"><i class="ph-television ph-bold ph-lg"></i> {{ appearNote.channel.name }}</MkA>
</div> </div>
@ -291,17 +291,17 @@ function blur() {
os.api('notes/children', { os.api('notes/children', {
noteId: appearNote.id, noteId: appearNote.id,
limit: 30, limit: 30,
depth: 6, depth: 12,
}).then(res => { }).then(res => {
replies.value = res; replies.value = res;
directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id); directReplies.value = res.filter(note => note.replyId === appearNote.id || note.renoteId === appearNote.id).reverse();
}); });
if (appearNote.replyId) { if (appearNote.replyId) {
os.api('notes/conversation', { os.api('notes/conversation', {
noteId: appearNote.replyId, noteId: appearNote.replyId,
}).then(res => { }).then(res => {
conversation.value = res.reverse(); conversation.value = res;
}); });
} }
@ -335,7 +335,6 @@ onUnmounted(() => {
.lxwezrsl { .lxwezrsl {
position: relative; position: relative;
transition: box-shadow 0.1s ease; transition: box-shadow 0.1s ease;
overflow: hidden;
contain: content; contain: content;
&:focus-visible { &:focus-visible {
@ -429,7 +428,12 @@ onUnmounted(() => {
> .article { > .article {
padding: 32px; padding: 32px;
padding-bottom: 6px;
&:last-child {
padding-bottom: 24px;
}
font-size: 1.2em; font-size: 1.2em;
overflow: clip;
> .header { > .header {
display: flex; display: flex;
@ -530,6 +534,10 @@ onUnmounted(() => {
padding: 16px; padding: 16px;
border: solid 1px var(--renote); border: solid 1px var(--renote);
border-radius: 8px; border-radius: 8px;
transition: background .2s;
&:hover, &:focus-within {
background-color: var(--panelHighlight);
}
} }
} }
} }
@ -577,26 +585,72 @@ onUnmounted(() => {
> .reply { > .reply {
border-top: solid 0.5px var(--divider); border-top: solid 0.5px var(--divider);
cursor: pointer; cursor: pointer;
padding-top: 24px;
padding-bottom: 10px;
@media (pointer: coarse) { @media (pointer: coarse) {
cursor: default; cursor: default;
} }
} }
> .reply, .reply-to, .reply-to-more { // Hover
transition: background-color 0.25s ease-in-out; .reply :deep(.main), .reply-to, .reply-to-more, :deep(.more) {
position: relative;
&:hover { &::before {
background-color: var(--panelHighlight); content: "";
position: absolute;
inset: -12px -24px;
bottom: -0px;
background: var(--panelHighlight);
border-radius: var(--radius);
opacity: 0;
transition: opacity .2s;
z-index: -1;
} }
&.reply-to, &.reply-to-more {
&::before {
inset: 0px 8px;
}
&:first-of-type::before {
top: 12px;
}
}
// &::after {
// content: "";
// position: absolute;
// inset: -9999px;
// background: var(--modalBg);
// opacity: 0;
// z-index: -2;
// pointer-events: none;
// transition: opacity .2s;
// }
&.more::before {
inset: 0 !important;
}
&:hover, &:focus-within {
&::before {
opacity: 1;
}
}
// @media (pointer: coarse) {
// &:has(.button:focus-within) {
// z-index: 2;
// --X13: transparent;
// &::after {
// opacity: 1;
// backdrop-filter: var(--modalBgFilter);
// }
// }
// }
} }
&.max-width_500px { &.max-width_500px {
font-size: 0.9em; font-size: 0.9em;
} }
&.max-width_450px { &.max-width_450px {
> .reply-to-more:first-child { > .reply-to-more:first-child {
padding-top: 14px; padding-top: 14px;
} }

View file

@ -0,0 +1,164 @@
<template>
<footer ref="el" class="footer" @click.stop>
<XReactionsViewer 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="directReplies > 0">
<p class="count">{{ directReplies }}</p>
</template>
<template v-else-if="appearNote.repliesCount > 0">
<p class="count">{{ appearNote.repliesCount }}</p>
</template>
</button>
<XRenoteButton ref="renoteButton" class="button" :note="appearNote" :count="appearNote.renoteCount"/>
<XStarButton v-if="appearNote.myReaction == null" ref="starButton" class="button" :note="appearNote"/>
<button v-if="appearNote.myReaction == null" 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="appearNote.myReaction != null" ref="reactButton" class="button _button reacted" @click="undoReact(appearNote)">
<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>
</template>
<script lang="ts" setup>
import { inject, ref } from 'vue';
import type { Ref } from 'vue';
import type * as misskey from 'calckey-js';
import XReactionsViewer from '@/components/MkReactionsViewer.vue';
import XStarButton from '@/components/MkStarButton.vue';
import XRenoteButton from '@/components/MkRenoteButton.vue';
import XQuoteButton from '@/components/MkQuoteButton.vue';
import { pleaseLogin } from '@/scripts/please-login';
import * as os from '@/os';
import { reactionPicker } from '@/scripts/reaction-picker';
import { i18n } from '@/i18n';
import { getNoteMenu } from '@/scripts/get-note-menu';
import { deepClone } from '@/scripts/clone';
import { useNoteCapture } from '@/scripts/use-note-capture';
const props = defineProps<{
note: misskey.entities.Note;
directReplies;
}>();
let note = $ref(deepClone(props.note));
const isRenote = (
note.renote != null &&
note.text == null &&
note.fileIds.length === 0 &&
note.poll == null
);
const el = ref<HTMLElement>();
const menuButton = ref<HTMLElement>();
const starButton = ref<InstanceType<typeof XStarButton>>();
const renoteButton = ref<InstanceType<typeof XRenoteButton>>();
const reactButton = ref<HTMLElement>();
let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note : note);
const isDeleted = ref(false);
const translation = ref(null);
const translating = ref(false);
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 menu(viaKeyboard = false): void {
os.popupMenu(getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClipPage }), menuButton.value, {
viaKeyboard,
}).then(focus);
}
function focus() {
el.value.focus();
}
function blur() {
el.value.blur();
}
</script>
<style lang="scss" scoped>
.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
> .button {
margin: 0;
padding: 8px;
opacity: 0.7;
flex-grow: 1;
max-width: 3.5em;
width: max-content;
min-width: max-content;
pointer-events: all;
transition: opacity .2s;
&:first-of-type {
margin-left: -.5em;
}
&:hover {
color: var(--fgHighlighted);
}
> .count {
display: inline;
margin: 0 0 0 8px;
opacity: 0.7;
}
&.reacted {
color: var(--accent);
}
}
}
</style>

View file

@ -2,7 +2,7 @@
<header class="kkwtjztg"> <header class="kkwtjztg">
<div class="user-info"> <div class="user-info">
<div> <div>
<MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)"> <MkA v-user-preview="note.user.id" class="name" :to="userPage(note.user)" @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="note.user.isBot" class="is-bot">bot</span>
</MkUserName> </MkUserName>
@ -47,6 +47,8 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
<style lang="scss" scoped> <style lang="scss" scoped>
.kkwtjztg { .kkwtjztg {
position: relative;
z-index: 2;
display: flex; display: flex;
align-items: center; align-items: center;
white-space: nowrap; white-space: nowrap;

View file

@ -1,9 +1,10 @@
<template> <template>
<div v-size="{ max: [450] }" class="wrpstxzv" :class="{ children: depth > 1 }"> <div v-size="{ max: [450, 500] }" class="wrpstxzv" :class="{ children: depth > 1, singleStart: replies.length == 1, firstColumn: depth == 1 && conversation }">
<div v-if="conversation && depth > 1" class="line"></div>
<div class="main" @click="router.push(notePage(note))"> <div class="main" @click="router.push(notePage(note))">
<div class="avatar-container"> <div class="avatar-container">
<MkAvatar class="avatar" :user="note.user"/> <MkAvatar class="avatar" :user="note.user"/>
<div class="line"></div> <div v-if="(!conversation) || replies.length > 0" class="line"></div>
</div> </div>
<div class="body"> <div class="body">
<XNoteHeader class="header" :note="note" :mini="true"/> <XNoteHeader class="header" :note="note" :mini="true"/>
@ -13,16 +14,21 @@
<XCwButton v-model="showContent" :note="note"/> <XCwButton v-model="showContent" :note="note"/>
</p> </p>
<div v-show="note.cw == null || showContent" class="content" @click="router.push(notePage(note))"> <div v-show="note.cw == null || showContent" class="content" @click="router.push(notePage(note))">
<MkSubNoteContent class="text" :note="note"/> <MkSubNoteContent class="text" :note="note" :detailed="true"/>
</div> </div>
</div> </div>
<MkNoteFooter :note="note" :directReplies="replies.length"></MkNoteFooter>
</div> </div>
</div> </div>
<template v-if="conversation"> <template v-if="conversation">
<template v-if="depth < 5"> <template v-if="replies.length == 1">
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply single" :conversation="conversation" :depth="depth"/>
</template>
<template v-else-if="depth < 5">
<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1"/> <MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" class="reply" :conversation="conversation" :depth="depth + 1"/>
</template> </template>
<div v-else-if="replies.length > 0" class="more"> <div v-else-if="replies.length > 0" class="more">
<div class="line"></div>
<MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA> <MkA class="text _link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
</div> </div>
</template> </template>
@ -35,6 +41,7 @@ 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 XCwButton from '@/components/MkCwButton.vue'; import XCwButton from '@/components/MkCwButton.vue';
import MkNoteFooter from '@/components/MkNoteFooter.vue';
import { notePage } from '@/filters/note'; import { notePage } from '@/filters/note';
import { useRouter } from '@/router'; import { useRouter } from '@/router';
import * as os from '@/os'; import * as os from '@/os';
@ -53,16 +60,15 @@ const props = withDefaults(defineProps<{
}); });
let showContent = $ref(false); let showContent = $ref(false);
const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id) ?? []; const replies: misskey.entities.Note[] = props.conversation?.filter(item => item.replyId === props.note.id || item.renoteId === props.note.id).reverse() ?? [];
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.wrpstxzv { .wrpstxzv {
padding: 16px 32px; padding: 16px 32px;
&.children { &.children {
padding: 10px 0 0 16px; padding: 10px 0 0 var(--indent);
padding-left: var(--indent) !important;
font-size: 1em; font-size: 1em;
cursor: auto; cursor: auto;
@ -71,6 +77,7 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
} }
} }
> .main { > .main {
display: flex; display: flex;
@ -89,6 +96,9 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
flex: 1; flex: 1;
min-width: 0; min-width: 0;
cursor: pointer; cursor: pointer;
margin: 0 -200px;
padding: 0 200px;
overflow: clip;
@media (pointer: coarse) { @media (pointer: coarse) {
cursor: default; cursor: default;
} }
@ -120,14 +130,66 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
} }
} }
} }
&:first-child > .main > .body {
margin-top: -200px;
padding-top: 200px;
}
&.reply {
--avatarSize: 38px;
.avatar-container {
margin-right: 8px !important;
}
:deep(.footer) {
font-size: .9em;
}
}
> .reply, > .more { > .reply, > .more {
border-left: solid 0.5px var(--divider);
margin-top: 10px; margin-top: 10px;
&.single {
padding: 0 !important;
> .line {
display: none;
}
}
} }
> .more { > .more {
padding: 10px 0 0 16px; display: flex;
padding-block: 10px;
font-weight: 600;
> .line {
flex-grow: 0 !important;
margin-top: -10px !important;
margin-bottom: 10px !important;
margin-right: 10px !important;
&::before {
border-left-style: dashed !important;
border-bottom-left-radius: 100px !important;
}
}
i {
font-size: 1em !important;
vertical-align: middle !important;
}
a {
position: static;
&::before {
content: "";
position: absolute;
inset: 0;
}
&::after {
content: unset;
}
}
}
&.reply, &.reply-to, &.reply-to-more {
> .main:hover, > .main:focus-within {
:deep(.footer .button) {
opacity: 1;
}
}
} }
&.reply-to, &.reply-to-more { &.reply-to, &.reply-to-more {
@ -135,41 +197,110 @@ const replies: misskey.entities.Note[] = props.conversation?.filter(item => item
&:first-child { &:first-child {
padding-top: 30px; padding-top: 30px;
} }
.avatar-container { .line::before {
display: flex; margin-bottom: -16px;
flex-direction: column; }
align-items: center; }
margin-right: 14px;
width: var(--avatarSize); // Reply Lines
> .avatar { &.reply, &.reply-to, &.reply-to-more {
width: var(--avatarSize); --indent: calc(var(--avatarSize) - 5px);
height: var(--avatarSize); > .main {
margin: 0; > .avatar-container {
}
> .line {
width: var(--avatarSize);
display: flex; display: flex;
flex-grow: 1; flex-direction: column;
&::before { align-items: center;
content: ""; margin-right: 14px;
display: block; width: var(--avatarSize);
width: 2px; > .avatar {
background-color: var(--divider); width: var(--avatarSize);
margin-inline: auto; height: var(--avatarSize);
.note > & { margin: 0;
margin-bottom: -16px;
}
} }
} }
} }
> .main > .body { .line {
padding-bottom: 16px; position: relative;
width: var(--avatarSize);
display: flex;
flex-grow: 1;
margin-bottom: -10px;
&::before {
content: "";
position: absolute;
border-left: 2px solid var(--X13);
margin-left: calc((var(--avatarSize) / 2) - 1px);
width: calc(var(--indent) / 2);
inset-block: 0;
min-height: 8px;
}
}
}
&.reply-to, &.reply-to-more {
> .main > .avatar-container > .line {
margin-bottom: 0px !important;
}
}
&.single, &.singleStart {
> .main > .avatar-container > .line {
margin-bottom: -10px !important;
}
}
.reply.children:not(:last-child) { // Line that goes through multiple replies
position: relative;
> .line {
position: absolute;
top: 0;
left: 0;
bottom: 0;
}
}
// Reply line connectors
.reply.children:not(.single) {
position: relative;
> .line {
position: absolute;
left: 0;
top: 0;
&::after {
content: "";
position: absolute;
border-left: 2px solid var(--X13);
border-bottom: 2px solid var(--X13);
margin-left: calc((var(--avatarSize) / 2) - 1px);
width: calc(var(--indent) / 2);
height: calc((var(--avatarSize) / 2));
border-bottom-left-radius: calc(var(--indent) / 2);
top: 8px;
}
}
&:not(:last-child) > .line::after {
mask: linear-gradient(to right, transparent 2px, black 2px);
-webkit-mask: linear-gradient(to right, transparent 2px, black 2px);
} }
} }
&.max-width_500px {
:not(.reply) > & {
.reply {
--avatarSize: 24px;
--indent: calc(var(--avatarSize) - 4px);
}
}
&.firstColumn {
> .main, > .line, > .children:not(.single) > .line {
--avatarSize: 35px;
--indent: 35px;
}
> .children:not(.single) {
padding-left: 28px !important;
}
}
}
&.max-width_450px { &.max-width_450px {
padding: 14px 16px; padding: 14px 16px;
&.reply-to, &.reply-to-more { &.reply-to, &.reply-to-more {
padding: 14px 16px;
padding-top: 14px !important; padding-top: 14px !important;
padding-bottom: 0 !important; padding-bottom: 0 !important;
margin-bottom: 0 !important; margin-bottom: 0 !important;

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="tivcixzd" :class="{ done: closed || isVoted }"> <div class="tivcixzd" :class="{ done: closed || isVoted }">
<ul> <ul>
<li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click="vote(i)"> <li v-for="(choice, i) in note.poll.choices" :key="i" :class="{ voted: choice.voted }" @click.stop="vote(i)">
<div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div> <div class="backdrop" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span> <span>
<template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template> <template v-if="choice.isVoted"><i class="ph-check ph-bold ph-lg"></i></template>
@ -13,7 +13,7 @@
<p v-if="!readOnly"> <p v-if="!readOnly">
<span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span> <span>{{ i18n.t('_poll.totalVotes', { n: total }) }}</span>
<span> · </span> <span> · </span>
<a v-if="!closed && !isVoted" @click="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a> <a v-if="!closed && !isVoted" @click.stop="showResult = !showResult">{{ showResult ? i18n.ts._poll.vote : i18n.ts._poll.showResult }}</a>
<span v-if="isVoted">{{ i18n.ts._poll.voted }}</span> <span v-if="isVoted">{{ i18n.ts._poll.voted }}</span>
<span v-else-if="closed">{{ i18n.ts._poll.closed }}</span> <span v-else-if="closed">{{ i18n.ts._poll.closed }}</span>
<span v-if="remaining > 0"> · {{ timer }}</span> <span v-if="remaining > 0"> · {{ timer }}</span>

View file

@ -22,7 +22,7 @@
<span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></span> <span v-if="visibility === 'specified'"><i class="ph-envelope-simple-open ph-bold ph-lg"></i></span>
</button> </button>
<button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code ph-bold ph-lg"></i></button> <button v-tooltip="i18n.ts.previewNoteText" class="_button preview" :class="{ active: showPreview }" @click="showPreview = !showPreview"><i class="ph-file-code ph-bold ph-lg"></i></button>
<button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-bend-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button> <button class="submit _buttonGradate" :disabled="!canPost" data-cy-open-post-form-submit @click="post">{{ submitText }}<i :class="reply ? 'ph-arrow-u-up-left ph-bold ph-lg' : renote ? 'ph-quotes ph-bold ph-lg' : 'ph-paper-plane-tilt ph-bold ph-lg'"></i></button>
</div> </div>
</header> </header>
<div class="form" :class="{ fixed }"> <div class="form" :class="{ fixed }">
@ -796,6 +796,8 @@ onMounted(() => {
} }
> .submit { > .submit {
display: inline-flex;
align-items: center;
margin: 16px 16px 16px 0; margin: 16px 16px 16px 0;
padding: 0 12px; padding: 0 12px;
line-height: 34px; line-height: 34px;

View file

@ -4,7 +4,7 @@
ref="buttonRef" ref="buttonRef"
v-ripple="canToggle" v-ripple="canToggle"
class="hkzvhatu _button" class="hkzvhatu _button"
:class="{ reacted: note.myReaction == reaction, canToggle }" :class="{ reacted: note.myReaction == reaction, canToggle, newlyAdded: !isInitial }"
@click="toggleReaction()" @click="toggleReaction()"
> >
<XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/> <XReactionIcon class="icon" :reaction="reaction" :custom-emojis="note.emojis"/>
@ -13,7 +13,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import * as misskey from 'calckey-js'; import * as misskey from 'calckey-js';
import XDetails from '@/components/MkReactionsViewer.details.vue'; import XDetails from '@/components/MkReactionsViewer.details.vue';
import XReactionIcon from '@/components/MkReactionIcon.vue'; import XReactionIcon from '@/components/MkReactionIcon.vue';
@ -55,20 +55,6 @@ const toggleReaction = () => {
} }
}; };
const anime = () => {
if (document.hidden) return;
// TODO:
};
watch(() => props.count, (newCount, oldCount) => {
if (oldCount < newCount) anime();
});
onMounted(() => {
if (!props.isInitial) anime();
});
useTooltip(buttonRef, async (showing) => { useTooltip(buttonRef, async (showing) => {
const reactions = await os.apiGet('notes/reactions', { const reactions = await os.apiGet('notes/reactions', {
noteId: props.note.id, noteId: props.note.id,
@ -97,7 +83,25 @@ useTooltip(buttonRef, async (showing) => {
margin: 2px; margin: 2px;
padding: 0 6px; padding: 0 6px;
border-radius: 4px; border-radius: 4px;
pointer-events: all;
&.newlyAdded {
animation: scaleInSmall .3s cubic-bezier(0,0,0,1.2);
:deep(.mk-emoji) {
animation: scaleIn .4s cubic-bezier(0.7, 0, 0, 1.5);
}
}
:deep(.mk-emoji) {
transition: transform .4s cubic-bezier(0,0,0,6);
}
&.reacted :deep(.mk-emoji) {
transition: transform .4s cubic-bezier(0,0,0,1);
}
&:active {
:deep(.mk-emoji) {
transition: transform .4s cubic-bezier(0,0,0,1);
transform: scale(.85);
}
}
&.canToggle { &.canToggle {
background: rgba(0, 0, 0, 0.05); background: rgba(0, 0, 0, 0.05);
@ -119,6 +123,7 @@ useTooltip(buttonRef, async (showing) => {
> .count { > .count {
color: var(--fgOnAccent); color: var(--fgOnAccent);
font-weight: 600;
} }
> .icon { > .icon {

View file

@ -21,9 +21,10 @@ const isMe = computed(() => $i && $i.id === props.note.userId);
<style lang="scss" scoped> <style lang="scss" scoped>
.tdflqwzn { .tdflqwzn {
margin: 4px -2px 0 -2px; margin-inline: -2px;
margin-top: .2em;
width: 100%; width: 100%;
&:empty { &:empty {
display: none; display: none;
} }

View file

@ -6,18 +6,23 @@
<Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/> <Mfm v-if="note.text" :text="note.text" :author="note.user" :i="$i" :custom-emojis="note.emojis"/>
<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">{{ i18n.ts.quoteAttached }}: ...</MkA> <MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">{{ i18n.ts.quoteAttached }}: ...</MkA>
</div> </div>
<template v-if="detailed">
<!-- <div v-if="note.renoteId" class="renote">
<XNoteSimple :note="note.renote"/>
</div> -->
<MkUrlPreview v-for="url in urls" :key="url" :url="url" :compact="true" :detail="false" class="url-preview"/>
</template>
<div v-if="note.files.length > 0"> <div v-if="note.files.length > 0">
<summary>({{ i18n.t('withNFiles', { n: note.files.length }) }})</summary>
<XMediaList :media-list="note.files"/> <XMediaList :media-list="note.files"/>
</div> </div>
<div v-if="note.poll"> <div v-if="note.poll">
<summary>{{ i18n.ts.poll }}</summary> <summary>{{ i18n.ts.poll }}</summary>
<XPoll :note="note"/> <XPoll :note="note"/>
</div> </div>
<button v-if="isLong && collapsed" class="fade _button" @click.stop.prevent="collapsed = false"> <button v-if="isLong && collapsed" class="fade _button" @click.stop="collapsed = false">
<span>{{ i18n.ts.showMore }}</span> <span>{{ i18n.ts.showMore }}</span>
</button> </button>
<button v-if="isLong && !collapsed" class="showLess _button" @click.stop.prevent="collapsed = true"> <button v-if="isLong && !collapsed" class="showLess _button" @click.stop="collapsed = true">
<span>{{ i18n.ts.showLess }}</span> <span>{{ i18n.ts.showLess }}</span>
</button> </button>
</div> </div>
@ -26,15 +31,19 @@
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as misskey from 'calckey-js'; import * as misskey from 'calckey-js';
import * as mfm from 'mfm-js';
import XNoteSimple from '@/components/MkNoteSimple.vue';
import XMediaList from '@/components/MkMediaList.vue'; import XMediaList from '@/components/MkMediaList.vue';
import XPoll from '@/components/MkPoll.vue'; import XPoll from '@/components/MkPoll.vue';
import MkUrlPreview from '@/components/MkUrlPreview.vue';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm';
import { i18n } from '@/i18n'; import { i18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
note: misskey.entities.Note; note: misskey.entities.Note;
detailed?: boolean;
}>(); }>();
const isLong = ( const isLong = (
props.note.cw == null && props.note.text != null && ( props.note.cw == null && props.note.text != null && (
(props.note.text.split('\n').length > 9) || (props.note.text.split('\n').length > 9) ||
@ -42,12 +51,14 @@ const isLong = (
) )
); );
const collapsed = $ref(props.note.cw == null && isLong); const collapsed = $ref(props.note.cw == null && isLong);
const urls = props.note.text ? extractUrlFromMfm(mfm.parse(props.note.text)) : null;
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.wrmlmaau { .wrmlmaau {
overflow-wrap: break-word; overflow-wrap: break-word;
> .body { > .body {
> .reply { > .reply {
margin-right: 6px; margin-right: 6px;
@ -61,6 +72,10 @@ const collapsed = $ref(props.note.cw == null && isLong);
} }
} }
> .mk-url-preview {
margin-top: 8px;
}
&.collapsed { &.collapsed {
position: relative; position: relative;
max-height: 9em; max-height: 9em;

View file

@ -1,12 +1,12 @@
<template> <template>
<div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`"> <div v-if="playerEnabled" class="player" :style="`padding: ${(player.height || 0) / (player.width || 1) * 100}% 0 0`" @click.stop>
<button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x ph-bold ph-lg"></i></button> <button class="disablePlayer" :title="i18n.ts.disablePlayer" @click="playerEnabled = false"><i class="ph-x ph-bold ph-lg"></i></button>
<iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/> <iframe :src="player.url + (player.url.match(/\?/) ? '&autoplay=1&auto_play=1' : '?autoplay=1&auto_play=1')" :width="player.width || '100%'" :heigth="player.height || 250" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen/>
</div> </div>
<div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter"> <div v-else-if="tweetId && tweetExpanded" ref="twitter" class="twitter" @click.stop>
<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe> <iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${$store.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
</div> </div>
<div v-else v-size="{ max: [400, 350] }" class="mk-url-preview"> <div v-else v-size="{ max: [400, 350] }" class="mk-url-preview" @click.stop>
<transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in"> <transition :name="$store.state.animation ? 'zoom' : ''" mode="out-in">
<component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url"> <component :is="self ? 'MkA' : 'a'" v-if="!fetching" class="link" :class="{ compact }" :[attr]="self ? url.substr(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
<div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`"> <div v-if="thumbnail" class="thumbnail" :style="`background-image: url('${thumbnail}')`">
@ -214,9 +214,10 @@ onUnmounted(() => {
border: 1px solid var(--divider); border: 1px solid var(--divider);
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
transition: background .2s;
&:hover { &:hover, &:focus-within {
text-decoration: none; text-decoration: none;
background-color: var(--panelHighlight);
> article > header > h1 { > article > header > h1 {
text-decoration: underline; text-decoration: underline;
} }

View file

@ -3,7 +3,7 @@
<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"/> <MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
</span> </span>
<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target"> <MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target" @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"/> <MkUserOnlineIndicator v-if="showIndicator && user.instance == null" class="indicator" :user="user"/>
</MkA> </MkA>

View file

@ -1,7 +1,7 @@
<template> <template>
<component <component
:is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target" :is="self ? 'MkA' : 'a'" ref="el" class="ieqqeuvs _link" :[attr]="self ? url.substr(local.length) : url" :rel="rel" :target="target"
@contextmenu.stop="() => {}" @contextmenu.stop="() => {}" @click.stop
> >
<template v-if="!self"> <template v-if="!self">
<span class="schema">{{ schema }}//</span> <span class="schema">{{ schema }}//</span>

View file

@ -2,7 +2,7 @@
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :content-max="800"> <MkSpacer :content-max="800">
<div class="fcuexfpr"> <div class="fcuexfpr" v-size="{ max: [500, 350] }">
<transition :name="$store.state.animation ? 'fade' : ''" mode="out-in"> <transition :name="$store.state.animation ? 'fade' : ''" mode="out-in">
<div v-if="note" class="note"> <div v-if="note" class="note">
<div v-if="showNext" class="_gap"> <div v-if="showNext" class="_gap">
@ -202,5 +202,13 @@ definePageMetadata(computed(() => note ? {
} }
} }
} }
#calckey_app > :not(.mk-deck) {
&.max-width_500px > .note {
margin-inline: -24px;
}
&.max-width_350px > .note {
margin-inline: -12px;
}
}
} }
</style> </style>

View file

@ -32,7 +32,7 @@ html {
overflow-wrap: break-word; overflow-wrap: break-word;
font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; font-family: "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
font-size: 14px; font-size: 14px;
line-height: 1.35; line-height: 1.6;
text-size-adjust: 100%; text-size-adjust: 100%;
tab-size: 2; tab-size: 2;
@ -155,6 +155,10 @@ hr {
box-shadow: 0px 4px 32px var(--shadow) !important; box-shadow: 0px 4px 32px var(--shadow) !important;
} }
.swiper {
overflow: clip !important;
}
._button { ._button {
appearance: none; appearance: none;
display: inline-block; display: inline-block;
@ -479,6 +483,7 @@ hr {
} }
._link { ._link {
position: relative;
color: var(--link); color: var(--link);
&:after { &:after {
@ -680,3 +685,19 @@ hr {
width: 1.25em; width: 1.25em;
display: inline-flex; display: inline-flex;
} }
@media(prefers-reduced-motion: no-preference) {
@keyframes scaleIn {
from {
transform: scale(0);
opacity: 0;
}
}
@keyframes scaleInSmall {
from {
transform: scale(.8);
opacity: 0;
}
}
}