mirror of
				https://codeberg.org/yeentown/barkey.git
				synced 2025-11-04 07:24:13 +00:00 
			
		
		
		
	upd: add option to select between note designs
Adds the ability to choose between `Sharkey` or `Misskey`
This commit is contained in:
		
							parent
							
								
									1c75339471
								
							
						
					
					
						commit
						f0fe8eceaf
					
				
					 16 changed files with 3404 additions and 323 deletions
				
			
		| 
						 | 
				
			
			@ -35,48 +35,51 @@ const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(p
 | 
			
		|||
const themeColor = instance.themeColor ?? '#777777';
 | 
			
		||||
 | 
			
		||||
const bg = {
 | 
			
		||||
	//background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
 | 
			
		||||
	background: `${themeColor}`,
 | 
			
		||||
	background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
$height: 2ex;
 | 
			
		||||
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	height: 1.5ex;
 | 
			
		||||
	border-radius: var(--radius-xl);
 | 
			
		||||
	margin-top: 5px;
 | 
			
		||||
	padding: 4px;
 | 
			
		||||
	height: $height;
 | 
			
		||||
	border-radius: var(--radius-xs) 0 0 var(--radius-xs);
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
	color: #fff;
 | 
			
		||||
	text-shadow: -1px -1px 0 var(--bg),1px -1px 0 var(--bg),-1px 1px 0 var(--bg),1px 1px 0 var(--bg)
 | 
			
		||||
	text-shadow: /* .866 ≈ sin(60deg) */
 | 
			
		||||
		1px 0 1px #000,
 | 
			
		||||
		.866px .5px 1px #000,
 | 
			
		||||
		.5px .866px 1px #000,
 | 
			
		||||
		0 1px 1px #000,
 | 
			
		||||
		-.5px .866px 1px #000,
 | 
			
		||||
		-.866px .5px 1px #000,
 | 
			
		||||
		-1px 0 1px #000,
 | 
			
		||||
		-.866px -.5px 1px #000,
 | 
			
		||||
		-.5px -.866px 1px #000,
 | 
			
		||||
		0 -1px 1px #000,
 | 
			
		||||
		.5px -.866px 1px #000,
 | 
			
		||||
		.866px -.5px 1px #000;
 | 
			
		||||
	mask-image: linear-gradient(90deg,
 | 
			
		||||
		rgb(0,0,0),
 | 
			
		||||
		rgb(0,0,0) calc(100% - 16px),
 | 
			
		||||
		rgba(0,0,0,0) 100%
 | 
			
		||||
	);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
	height: 2ex;
 | 
			
		||||
	height: $height;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.name {
 | 
			
		||||
	margin-left: 4px;
 | 
			
		||||
	line-height: 1;
 | 
			
		||||
	font-size: 0.8em;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	overflow-wrap: anywhere;
 | 
			
		||||
	max-width: 300px;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 400px) {
 | 
			
		||||
	.name {
 | 
			
		||||
		max-width: 50px;
 | 
			
		||||
	}
 | 
			
		||||
	overflow: visible;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -47,14 +47,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<Mfm :text="getNoteSummary(appearNote)" :plain="true" :nowrap="true" :author="appearNote.user" :nyaize="'respect'" :class="$style.collapsedRenoteTargetText" @click="renoteCollapsed = false"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<article v-else :class="$style.article" @contextmenu.stop="onContextmenu">
 | 
			
		||||
		<div style="display: flex; padding-bottom: 10px;">
 | 
			
		||||
		<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
 | 
			
		||||
			<MkAvatar :class="[$style.avatar, { [$style.avatarReplyTo]: appearNote.reply }]" :user="appearNote.user" :link="!mock" :preview="!mock"/>
 | 
			
		||||
			<div :class="$style.main">
 | 
			
		||||
				<MkNoteHeader :note="appearNote" :mini="true"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div :class="[{ [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
 | 
			
		||||
		<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
 | 
			
		||||
		<div :class="[$style.main, { [$style.clickToOpen]: defaultStore.state.clickToOpen }]" @click="defaultStore.state.clickToOpen ? noteclick(appearNote.id) : undefined">
 | 
			
		||||
			<MkNoteHeader :note="appearNote" :mini="true" v-on:click.stop/>
 | 
			
		||||
			<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
 | 
			
		||||
			<div style="container-type: inline-size;">
 | 
			
		||||
				<p v-if="appearNote.cw != null" :class="$style.cw">
 | 
			
		||||
					<Mfm v-if="appearNote.cw != ''" style="margin-right: 8px;" :text="appearNote.cw" :author="appearNote.user" :nyaize="'respect'"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +60,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
				<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]" >
 | 
			
		||||
					<div :class="$style.text">
 | 
			
		||||
						<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
 | 
			
		||||
						<MkA v-if="appearNote.replyId" :class="$style.replyIcon" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
 | 
			
		||||
						<Mfm
 | 
			
		||||
							v-if="appearNote.text"
 | 
			
		||||
							:parsedNodes="parsed"
 | 
			
		||||
| 
						 | 
				
			
			@ -178,6 +176,7 @@ import MkCwButton from '@/components/MkCwButton.vue';
 | 
			
		|||
import MkPoll from '@/components/MkPoll.vue';
 | 
			
		||||
import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 | 
			
		||||
import MkUrlPreview from '@/components/MkUrlPreview.vue';
 | 
			
		||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { pleaseLogin } from '@/scripts/please-login.js';
 | 
			
		||||
import { focusPrev, focusNext } from '@/scripts/focus.js';
 | 
			
		||||
| 
						 | 
				
			
			@ -831,7 +830,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
	position: relative;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	padding: 24px 38px 16px;
 | 
			
		||||
	padding: 16px 32px 8px 32px;
 | 
			
		||||
	line-height: 28px;
 | 
			
		||||
	white-space: pre;
 | 
			
		||||
	color: var(--renote);
 | 
			
		||||
| 
						 | 
				
			
			@ -883,7 +882,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
	align-items: center;
 | 
			
		||||
	line-height: 28px;
 | 
			
		||||
	white-space: pre;
 | 
			
		||||
	padding: 8px 38px 24px;
 | 
			
		||||
	padding: 0 32px 18px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.collapsedRenoteTargetAvatar {
 | 
			
		||||
| 
						 | 
				
			
			@ -910,6 +909,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
 | 
			
		||||
.article {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	padding: 28px 32px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -926,19 +926,12 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
.avatar {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	display: block !important;
 | 
			
		||||
	position: sticky !important;
 | 
			
		||||
	margin: 0 14px 0 0;
 | 
			
		||||
	width: 58px;
 | 
			
		||||
	height: 58px;
 | 
			
		||||
	position: sticky !important;
 | 
			
		||||
	top: calc(22px + var(--stickyTop, 0px));
 | 
			
		||||
	left: 0;
 | 
			
		||||
	transition: top 0.5s;
 | 
			
		||||
 | 
			
		||||
	&.avatarReplyTo {
 | 
			
		||||
		position: relative !important;
 | 
			
		||||
		top: 0 !important;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
| 
						 | 
				
			
			@ -1001,6 +994,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
 | 
			
		||||
.text {
 | 
			
		||||
	overflow-wrap: break-word;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.replyIcon {
 | 
			
		||||
| 
						 | 
				
			
			@ -1033,8 +1027,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
 | 
			
		||||
.quoteNote {
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
	// Made border solid, stylistic choice
 | 
			
		||||
	border: solid 1px var(--renote);
 | 
			
		||||
	border: dashed 1px var(--renote);
 | 
			
		||||
	border-radius: var(--radius-sm);
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1074,11 +1067,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	.renote {
 | 
			
		||||
		padding: 24px 28px 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.collapsedRenoteTarget {
 | 
			
		||||
		padding: 8px 28px 24px;
 | 
			
		||||
		padding: 12px 26px 0 26px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.article {
 | 
			
		||||
| 
						 | 
				
			
			@ -1096,8 +1085,12 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
		font-size: 0.9em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.renote {
 | 
			
		||||
		padding: 10px 22px 0 22px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.article {
 | 
			
		||||
		padding: 23px 25px;
 | 
			
		||||
		padding: 20px 22px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.footer {
 | 
			
		||||
| 
						 | 
				
			
			@ -1107,7 +1100,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
 | 
			
		||||
@container (max-width: 480px) {
 | 
			
		||||
	.renote {
 | 
			
		||||
		padding: 20px 24px 8px;
 | 
			
		||||
		padding: 8px 16px 0 16px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.tip {
 | 
			
		||||
| 
						 | 
				
			
			@ -1115,12 +1108,12 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	.collapsedRenoteTarget {
 | 
			
		||||
		padding: 8px 24px 20px;
 | 
			
		||||
		padding: 0 16px 9px;
 | 
			
		||||
		margin-top: 4px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.article {
 | 
			
		||||
		padding: 22px 24px;
 | 
			
		||||
		padding: 14px 16px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,9 +11,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	v-hotkey="keymap"
 | 
			
		||||
	:class="$style.root"
 | 
			
		||||
>
 | 
			
		||||
	<div v-if="appearNote.reply && appearNote.reply.replyId && !conversationLoaded" style="padding: 16px">
 | 
			
		||||
	<div v-if="appearNote.reply && appearNote.reply.replyId">
 | 
			
		||||
		<div v-if="!conversationLoaded" style="padding: 16px">
 | 
			
		||||
			<MkButton style="margin: 0 auto;" primary rounded @click="loadConversation">{{ i18n.ts.loadConversation }}</MkButton>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	<div v-if="isRenote" :class="$style.renote">
 | 
			
		||||
		<MkAvatar :class="$style.renoteAvatar" :user="note.user" link preview/>
 | 
			
		||||
		<i class="ph-rocket-launch ph-bold ph-lg" style="margin-right: 4px;"></i>
 | 
			
		||||
| 
						 | 
				
			
			@ -39,29 +43,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<template v-if="appearNote.reply && appearNote.reply.replyId">
 | 
			
		||||
		<MkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	</template>
 | 
			
		||||
	<MkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	<article :class="$style.note" @contextmenu.stop="onContextmenu">
 | 
			
		||||
		<header :class="$style.noteHeader">
 | 
			
		||||
			<MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
 | 
			
		||||
			<div style="display: flex; align-items: center; white-space: nowrap; overflow: hidden;">
 | 
			
		||||
			<div :class="$style.noteHeaderBody">
 | 
			
		||||
				<div>
 | 
			
		||||
					<MkA v-user-preview="appearNote.user.id" :class="$style.noteHeaderName" :to="userPage(appearNote.user)">
 | 
			
		||||
						<MkUserName :nowrap="false" :user="appearNote.user"/>
 | 
			
		||||
					</MkA>
 | 
			
		||||
					<span v-if="appearNote.user.isBot" :class="$style.isBot">bot</span>
 | 
			
		||||
						<span v-if="appearNote.user.badgeRoles" :class="$style.badgeRoles">
 | 
			
		||||
							<img v-for="role in appearNote.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
 | 
			
		||||
						</span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<div style="display: flex; align-items: flex-end; margin-left: auto;">
 | 
			
		||||
				<div :class="$style.noteHeaderBody">
 | 
			
		||||
					<div :class="$style.noteHeaderInfo">
 | 
			
		||||
						<span v-if="appearNote.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[appearNote.visibility]">
 | 
			
		||||
							<i v-if="appearNote.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
 | 
			
		||||
| 
						 | 
				
			
			@ -71,8 +61,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
						<span v-if="appearNote.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil ph-bold ph-lg"></i></span>
 | 
			
		||||
						<span v-if="appearNote.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
 | 
			
		||||
					</div>
 | 
			
		||||
					<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div :class="$style.noteHeaderUsername"><MkAcct :user="appearNote.user"/></div>
 | 
			
		||||
				<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</header>
 | 
			
		||||
		<div :class="$style.noteContent">
 | 
			
		||||
| 
						 | 
				
			
			@ -82,6 +73,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			</p>
 | 
			
		||||
			<div v-show="appearNote.cw == null || showContent">
 | 
			
		||||
				<span v-if="appearNote.isHidden" style="opacity: 0.5">({{ i18n.ts.private }})</span>
 | 
			
		||||
				<MkA v-if="appearNote.replyId" :class="$style.noteReplyTarget" :to="`/notes/${appearNote.replyId}`"><i class="ph-arrow-bend-left-up ph-bold ph-lg"></i></MkA>
 | 
			
		||||
				<Mfm
 | 
			
		||||
					v-if="appearNote.text"
 | 
			
		||||
					:parsedNodes="parsed"
 | 
			
		||||
| 
						 | 
				
			
			@ -859,19 +851,12 @@ function animatedMFM() {
 | 
			
		|||
 | 
			
		||||
.noteHeaderInfo {
 | 
			
		||||
	float: right;
 | 
			
		||||
	text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.noteHeaderUsername {
 | 
			
		||||
	margin-bottom: 2px;
 | 
			
		||||
	line-height: 1.3;
 | 
			
		||||
	word-wrap: anywhere;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.playMFMButton {
 | 
			
		||||
| 
						 | 
				
			
			@ -1052,31 +1037,9 @@ function animatedMFM() {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.avatar {
 | 
			
		||||
	flex-shrink: 0 !important;
 | 
			
		||||
	display: block !important;
 | 
			
		||||
	margin: 0 10px 0 0 !important;
 | 
			
		||||
	width: 40px !important;
 | 
			
		||||
	height: 40px !important;
 | 
			
		||||
	border-radius: var(--radius-sm) !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.muted {
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	opacity: 0.7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badgeRoles {
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badgeRole {
 | 
			
		||||
	height: 1.3em;
 | 
			
		||||
	vertical-align: -20%;
 | 
			
		||||
 | 
			
		||||
	& + .badgeRole {
 | 
			
		||||
		margin-left: 0.2em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,9 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<header v-if="!classic" :class="$style.root">
 | 
			
		||||
	<div :class="$style.section">
 | 
			
		||||
		<div style="display: flex;">
 | 
			
		||||
<header :class="$style.root">
 | 
			
		||||
	<div v-if="mock" :class="$style.name">
 | 
			
		||||
		<MkUserName :user="note.user"/>
 | 
			
		||||
	</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -14,46 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<MkUserName :user="note.user"/>
 | 
			
		||||
	</MkA>
 | 
			
		||||
	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
 | 
			
		||||
			<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
 | 
			
		||||
				<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	<div :class="$style.username"><MkAcct :user="note.user"/></div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.section">
 | 
			
		||||
		<div :class="$style.info">
 | 
			
		||||
			<div v-if="mock">
 | 
			
		||||
				<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkA v-else :class="$style.time" :to="notePage(note)">
 | 
			
		||||
				<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
			</MkA>
 | 
			
		||||
			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
 | 
			
		||||
				<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
 | 
			
		||||
				<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
 | 
			
		||||
				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
 | 
			
		||||
			</span>
 | 
			
		||||
			<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil ph-bold ph-lg"></i></span>
 | 
			
		||||
			<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
 | 
			
		||||
			<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></i></span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div :class="$style.info"><MkInstanceTicker v-if="showTicker" style="cursor: pointer;" :instance="note.user.instance" @click.stop="showOnRemote()"/></div>
 | 
			
		||||
	</div>
 | 
			
		||||
</header>
 | 
			
		||||
<header v-else :class="$style.classicRoot">
 | 
			
		||||
	<div v-if="mock" :class="$style.name">
 | 
			
		||||
		<MkUserName :user="note.user"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkA v-else v-user-preview="note.user.id" :class="$style.classicName" :to="userPage(note.user)">
 | 
			
		||||
		<MkUserName :user="note.user"/>
 | 
			
		||||
	</MkA>
 | 
			
		||||
	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
 | 
			
		||||
	<div :class="$style.classicUsername"><MkAcct :user="note.user"/></div>
 | 
			
		||||
	<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
 | 
			
		||||
		<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkInstanceTicker v-if="showTicker && !isMobile && defaultStore.state.showTickerOnReplies" style="cursor: pointer; max-height: 5px; top: 3px; position: relative; margin-top: 0px !important;" :instance="note.user.instance" @click.stop="showOnRemote()"/>
 | 
			
		||||
	<div :class="$style.classicInfo">
 | 
			
		||||
	<div :class="$style.info">
 | 
			
		||||
		<div v-if="mock">
 | 
			
		||||
			<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
		</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -73,29 +36,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { inject, shallowRef, ref } from 'vue';
 | 
			
		||||
import { inject, shallowRef } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { notePage } from '@/filters/note.js';
 | 
			
		||||
import { userPage } from '@/filters/user.js';
 | 
			
		||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
 | 
			
		||||
import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
 | 
			
		||||
import { popupMenu } from '@/os.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { useRouter } from '@/router.js';
 | 
			
		||||
import { deviceKind } from '@/scripts/device-kind.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	classic?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const menuVersionsButton = shallowRef<HTMLElement>();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && props.note.user.instance);
 | 
			
		||||
 | 
			
		||||
const MOBILE_THRESHOLD = 500;
 | 
			
		||||
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
 | 
			
		||||
 | 
			
		||||
async function menuVersions(viaKeyboard = false): Promise<void> {
 | 
			
		||||
	const { menu, cleanup } = await getNoteVersionsMenu({ note: props.note, menuVersionsButton });
 | 
			
		||||
| 
						 | 
				
			
			@ -104,67 +57,18 @@ async function menuVersions(viaKeyboard = false): Promise<void> {
 | 
			
		|||
	}).then(focus).finally(cleanup);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showOnRemote() {
 | 
			
		||||
	if (props.note.url ?? props.note.uri === undefined) router.push(notePage(props.note));
 | 
			
		||||
	else window.open(props.note.url ?? props.note.uri);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mock = inject<boolean>('mock', false);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	cursor: auto; /* not clickToOpen-able */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicRoot {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: baseline;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	cursor: auto; /* not clickToOpen-able */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.section {
 | 
			
		||||
		align-items: flex-start;
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
 | 
			
		||||
		&:last-child {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			align-items: flex-end;
 | 
			
		||||
			margin-left: auto;
 | 
			
		||||
			padding-left: 10px;
 | 
			
		||||
			overflow: clip;
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.name {
 | 
			
		||||
	flex-shrink: 1;
 | 
			
		||||
	display: block;
 | 
			
		||||
	// note, these margin top values were done by hand may need futher checking if it actualy aligns pixel perfect
 | 
			
		||||
	margin: 3px .5em 0 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	overflow: scroll;
 | 
			
		||||
	overflow-wrap: anywhere;
 | 
			
		||||
	font-size: 1em;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	max-width: 300px;
 | 
			
		||||
 | 
			
		||||
		&::-webkit-scrollbar {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:hover {
 | 
			
		||||
			color: var(--nameHover);
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicName {
 | 
			
		||||
	flex-shrink: 1;
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -191,20 +95,6 @@ const mock = inject<boolean>('mock', false);
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.username {
 | 
			
		||||
	flex-shrink: 9999999;
 | 
			
		||||
	// note these top margins were made to align with the instance ticker
 | 
			
		||||
	margin: 4px .5em 0 0;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	font-size: .95em;
 | 
			
		||||
	max-width: 300px;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicUsername {
 | 
			
		||||
	flex-shrink: 9999999;
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
| 
						 | 
				
			
			@ -212,34 +102,11 @@ const mock = inject<boolean>('mock', false);
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.info {
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		margin-top: 4px;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	margin-left: auto;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	&:not(:first-child) {
 | 
			
		||||
		flex-shrink: 0;
 | 
			
		||||
		margin-left: auto;
 | 
			
		||||
		font-size: 0.9em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicInfo {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	margin-left: auto;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.time {
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badgeRoles {
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -252,14 +119,4 @@ const mock = inject<boolean>('mock', false);
 | 
			
		|||
		margin-left: 0.2em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.danger {
 | 
			
		||||
		color: var(--accent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@container (max-width: 500px) {
 | 
			
		||||
		.name, .username {
 | 
			
		||||
			max-width: 200px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<div :class="$style.root">
 | 
			
		||||
	<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
 | 
			
		||||
	<div :class="$style.main">
 | 
			
		||||
		<MkNoteHeader :class="$style.header" :classic="true" :note="note" :mini="true"/>
 | 
			
		||||
		<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
 | 
			
		||||
		<div>
 | 
			
		||||
			<p v-if="note.cw != null" :class="$style.cw">
 | 
			
		||||
				<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,12 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
 | 
			
		||||
	<div v-if="!hideLine" :class="$style.line"></div>
 | 
			
		||||
	<div :class="$style.main">
 | 
			
		||||
		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
 | 
			
		||||
		<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
 | 
			
		||||
		<div :class="$style.body">
 | 
			
		||||
			<MkNoteHeader :class="$style.header" :note="note" :classic="true" :mini="true"/>
 | 
			
		||||
			<MkNoteHeader :class="$style.header" :note="note" :mini="true"/>
 | 
			
		||||
			<div :class="$style.content">
 | 
			
		||||
				<p v-if="note.cw != null" :class="$style.cw">
 | 
			
		||||
					<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
 | 
			
		||||
| 
						 | 
				
			
			@ -107,7 +106,6 @@ import { getNoteMenu } from '@/scripts/get-note-menu.js';
 | 
			
		|||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
 | 
			
		||||
 | 
			
		||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 | 
			
		||||
const hideLine = computed(() => { return props.detail ? true : false; });
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
| 
						 | 
				
			
			@ -363,7 +361,7 @@ if (props.detail) {
 | 
			
		|||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	padding: 28px 32px;
 | 
			
		||||
	padding: 16px 32px;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -373,14 +371,6 @@ if (props.detail) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.line {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	left: 60px;
 | 
			
		||||
	// using solid instead of dotted, stylelistic choice
 | 
			
		||||
	border-left: 2.5px solid rgb(174, 174, 174);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.footer {
 | 
			
		||||
		position: relative;
 | 
			
		||||
		z-index: 1;
 | 
			
		||||
| 
						 | 
				
			
			@ -406,9 +396,9 @@ if (props.detail) {
 | 
			
		|||
.avatar {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 14px 0 0;
 | 
			
		||||
	width: 58px;
 | 
			
		||||
	height: 58px;
 | 
			
		||||
	margin: 0 8px 0 0;
 | 
			
		||||
	width: 38px;
 | 
			
		||||
	height: 38px;
 | 
			
		||||
	border-radius: var(--radius-sm);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -421,11 +411,6 @@ if (props.detail) {
 | 
			
		|||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
	margin-bottom: 2px;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -445,36 +430,6 @@ if (props.detail) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reply, .more {
 | 
			
		||||
	border-left: solid 0.5px var(--divider);
 | 
			
		||||
	margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more {
 | 
			
		||||
	padding: 10px 0 0 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 580px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: 28px 26px 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.line {
 | 
			
		||||
		left: 50.5px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.avatar {
 | 
			
		||||
		width: 50px;
 | 
			
		||||
		height: 50px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 500px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: 23px 25px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 400px) {
 | 
			
		||||
	.noteFooterButton {
 | 
			
		||||
		&:not(:last-child) {
 | 
			
		||||
| 
						 | 
				
			
			@ -514,9 +469,9 @@ if (props.detail) {
 | 
			
		|||
	padding: 10px 0 0 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 480px) {
 | 
			
		||||
@container (max-width: 450px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: 22px 24px;
 | 
			
		||||
		padding: 14px 16px;
 | 
			
		||||
 | 
			
		||||
		&.children {
 | 
			
		||||
			padding: 10px 0 0 8px;
 | 
			
		||||
| 
						 | 
				
			
			@ -524,17 +479,6 @@ if (props.detail) {
 | 
			
		|||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 450px) {
 | 
			
		||||
	.line {
 | 
			
		||||
		left: 46px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.avatar {
 | 
			
		||||
		width: 46px;
 | 
			
		||||
		height: 46px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.muted {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	padding: 8px !important;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
	<template #default="{ items: notes }">
 | 
			
		||||
		<div :class="[$style.root, { [$style.noGap]: noGap }]">
 | 
			
		||||
			<MkDateSeparatedList
 | 
			
		||||
				v-if="defaultStore.state.noteDesign === 'misskey'"
 | 
			
		||||
				ref="notes"
 | 
			
		||||
				v-slot="{ item: note }"
 | 
			
		||||
				:items="notes"
 | 
			
		||||
| 
						 | 
				
			
			@ -26,18 +27,35 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
			>
 | 
			
		||||
				<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
 | 
			
		||||
			</MkDateSeparatedList>
 | 
			
		||||
			<MkDateSeparatedList
 | 
			
		||||
				v-else-if="defaultStore.state.noteDesign === 'sharkey'"
 | 
			
		||||
				ref="notes" 
 | 
			
		||||
				v-slot="{ item: note }"
 | 
			
		||||
				:items="notes"
 | 
			
		||||
				:direction="pagination.reversed ? 'up' : 'down'"
 | 
			
		||||
				:reversed="pagination.reversed"
 | 
			
		||||
				:noGap="noGap"
 | 
			
		||||
				:ad="true"
 | 
			
		||||
				:class="$style.notes"
 | 
			
		||||
			>
 | 
			
		||||
				<SkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
 | 
			
		||||
			</MkDateSeparatedList>
 | 
			
		||||
		</div>
 | 
			
		||||
	</template>
 | 
			
		||||
</MkPagination>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { shallowRef } from 'vue';
 | 
			
		||||
import { shallowRef, ref } from 'vue';
 | 
			
		||||
import MkNote from '@/components/MkNote.vue';
 | 
			
		||||
import SkNote from '@/components/SkNote.vue';
 | 
			
		||||
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 | 
			
		||||
import MkPagination, { Paging } from '@/components/MkPagination.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { infoImageUrl } from '@/instance.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
console.log(defaultStore.state.noteDesign, defaultStore.state.noteDesign === 'sharkey');
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	pagination: Paging;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										82
									
								
								packages/frontend/src/components/SkInstanceTicker.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										82
									
								
								packages/frontend/src/components/SkInstanceTicker.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,82 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root" :style="bg">
 | 
			
		||||
	<img v-if="faviconUrl" :class="$style.icon" :src="faviconUrl"/>
 | 
			
		||||
	<div :class="$style.name">{{ instance.name }}</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { } from 'vue';
 | 
			
		||||
import { instanceName } from '@/config.js';
 | 
			
		||||
import { instance as Instance } from '@/instance.js';
 | 
			
		||||
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	instance?: {
 | 
			
		||||
		faviconUrl?: string
 | 
			
		||||
		name: string
 | 
			
		||||
		themeColor?: string
 | 
			
		||||
	}
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
// if no instance data is given, this is for the local instance
 | 
			
		||||
const instance = props.instance ?? {
 | 
			
		||||
	name: instanceName,
 | 
			
		||||
	themeColor: (document.querySelector('meta[name="theme-color-orig"]') as HTMLMetaElement).content,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const faviconUrl = $computed(() => props.instance ? getProxiedImageUrlNullable(props.instance.faviconUrl, 'preview') : getProxiedImageUrlNullable(Instance.iconUrl, 'preview') ?? getProxiedImageUrlNullable(Instance.faviconUrl, 'preview') ?? '/favicon.ico');
 | 
			
		||||
 | 
			
		||||
const themeColor = instance.themeColor ?? '#777777';
 | 
			
		||||
 | 
			
		||||
const bg = {
 | 
			
		||||
	//background: `linear-gradient(90deg, ${themeColor}, ${themeColor}00)`,
 | 
			
		||||
	background: `${themeColor}`,
 | 
			
		||||
};
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	height: 1.5ex;
 | 
			
		||||
	border-radius: var(--radius-xl);
 | 
			
		||||
	margin-top: 5px;
 | 
			
		||||
	padding: 4px;
 | 
			
		||||
	overflow: clip;
 | 
			
		||||
	color: #fff;
 | 
			
		||||
	text-shadow: -1px -1px 0 var(--bg),1px -1px 0 var(--bg),-1px 1px 0 var(--bg),1px 1px 0 var(--bg)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
	height: 2ex;
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.name {
 | 
			
		||||
	margin-left: 4px;
 | 
			
		||||
	line-height: 1;
 | 
			
		||||
	font-size: 0.8em;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	overflow-wrap: anywhere;
 | 
			
		||||
	max-width: 300px;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 400px) {
 | 
			
		||||
	.name {
 | 
			
		||||
		max-width: 50px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										1201
									
								
								packages/frontend/src/components/SkNote.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1201
									
								
								packages/frontend/src/components/SkNote.vue
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1082
									
								
								packages/frontend/src/components/SkNoteDetailed.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										1082
									
								
								packages/frontend/src/components/SkNoteDetailed.vue
									
										
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										265
									
								
								packages/frontend/src/components/SkNoteHeader.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										265
									
								
								packages/frontend/src/components/SkNoteHeader.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,265 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<header v-if="!classic" :class="$style.root">
 | 
			
		||||
	<div :class="$style.section">
 | 
			
		||||
		<div style="display: flex;">
 | 
			
		||||
			<div v-if="mock" :class="$style.name">
 | 
			
		||||
				<MkUserName :user="note.user"/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
 | 
			
		||||
				<MkUserName :user="note.user"/>
 | 
			
		||||
			</MkA>
 | 
			
		||||
			<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
 | 
			
		||||
			<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
 | 
			
		||||
				<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div :class="$style.username"><MkAcct :user="note.user"/></div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div :class="$style.section">
 | 
			
		||||
		<div :class="$style.info">
 | 
			
		||||
			<div v-if="mock">
 | 
			
		||||
				<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkA v-else :class="$style.time" :to="notePage(note)">
 | 
			
		||||
				<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
			</MkA>
 | 
			
		||||
			<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
 | 
			
		||||
				<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
 | 
			
		||||
				<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
 | 
			
		||||
				<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
 | 
			
		||||
			</span>
 | 
			
		||||
			<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil ph-bold ph-lg"></i></span>
 | 
			
		||||
			<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
 | 
			
		||||
			<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></i></span>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div :class="$style.info"><SkInstanceTicker v-if="showTicker" style="cursor: pointer;" :instance="note.user.instance" @click.stop="showOnRemote()"/></div>
 | 
			
		||||
	</div>
 | 
			
		||||
</header>
 | 
			
		||||
<header v-else :class="$style.classicRoot">
 | 
			
		||||
	<div v-if="mock" :class="$style.name">
 | 
			
		||||
		<MkUserName :user="note.user"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<MkA v-else v-user-preview="note.user.id" :class="$style.classicName" :to="userPage(note.user)">
 | 
			
		||||
		<MkUserName :user="note.user"/>
 | 
			
		||||
	</MkA>
 | 
			
		||||
	<div v-if="note.user.isBot" :class="$style.isBot">bot</div>
 | 
			
		||||
	<div :class="$style.classicUsername"><MkAcct :user="note.user"/></div>
 | 
			
		||||
	<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
 | 
			
		||||
		<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<SkInstanceTicker v-if="showTicker && !isMobile && defaultStore.state.showTickerOnReplies" style="cursor: pointer; max-height: 5px; top: 3px; position: relative; margin-top: 0px !important;" :instance="note.user.instance" @click.stop="showOnRemote()"/>
 | 
			
		||||
	<div :class="$style.classicInfo">
 | 
			
		||||
		<div v-if="mock">
 | 
			
		||||
			<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<MkA v-else :to="notePage(note)">
 | 
			
		||||
			<MkTime :time="note.createdAt" colored/>
 | 
			
		||||
		</MkA>
 | 
			
		||||
		<span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]">
 | 
			
		||||
			<i v-if="note.visibility === 'home'" class="ph-house ph-bold ph-lg"></i>
 | 
			
		||||
			<i v-else-if="note.visibility === 'followers'" class="ph-lock ph-bold ph-lg"></i>
 | 
			
		||||
			<i v-else-if="note.visibility === 'specified'" ref="specified" class="ph-envelope ph-bold ph-lg"></i>
 | 
			
		||||
		</span>
 | 
			
		||||
		<span v-if="note.updatedAt" ref="menuVersionsButton" style="margin-left: 0.5em; cursor: pointer;" title="Edited" @mousedown="menuVersions()"><i class="ph-pencil ph-bold ph-lg"></i></span>
 | 
			
		||||
		<span v-if="note.localOnly" style="margin-left: 0.5em;" :title="i18n.ts._visibility['disableFederation']"><i class="ph-rocket ph-bold ph-lg"></i></span>
 | 
			
		||||
		<span v-if="note.channel" style="margin-left: 0.5em;" :title="note.channel.name"><i class="ph-television ph-bold ph-lg"></i></span>
 | 
			
		||||
	</div>
 | 
			
		||||
</header>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { inject, shallowRef, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { notePage } from '@/filters/note.js';
 | 
			
		||||
import { userPage } from '@/filters/user.js';
 | 
			
		||||
import { getNoteVersionsMenu } from '@/scripts/get-note-versions-menu.js';
 | 
			
		||||
import SkInstanceTicker from '@/components/SkInstanceTicker.vue';
 | 
			
		||||
import { popupMenu } from '@/os.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { useRouter } from '@/router.js';
 | 
			
		||||
import { deviceKind } from '@/scripts/device-kind.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	classic?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const menuVersionsButton = shallowRef<HTMLElement>();
 | 
			
		||||
const router = useRouter();
 | 
			
		||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && props.note.user.instance);
 | 
			
		||||
 | 
			
		||||
const MOBILE_THRESHOLD = 500;
 | 
			
		||||
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);
 | 
			
		||||
 | 
			
		||||
async function menuVersions(viaKeyboard = false): Promise<void> {
 | 
			
		||||
	const { menu, cleanup } = await getNoteVersionsMenu({ note: props.note, menuVersionsButton });
 | 
			
		||||
	popupMenu(menu, menuVersionsButton.value, {
 | 
			
		||||
		viaKeyboard,
 | 
			
		||||
	}).then(focus).finally(cleanup);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function showOnRemote() {
 | 
			
		||||
	if (props.note.url ?? props.note.uri === undefined) router.push(notePage(props.note));
 | 
			
		||||
	else window.open(props.note.url ?? props.note.uri);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mock = inject<boolean>('mock', false);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	cursor: auto; /* not clickToOpen-able */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicRoot {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: baseline;
 | 
			
		||||
	white-space: nowrap;
 | 
			
		||||
	cursor: auto; /* not clickToOpen-able */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.section {
 | 
			
		||||
		align-items: flex-start;
 | 
			
		||||
		white-space: nowrap;
 | 
			
		||||
		flex-direction: column;
 | 
			
		||||
		overflow: hidden;
 | 
			
		||||
 | 
			
		||||
		&:last-child {
 | 
			
		||||
			display: flex;
 | 
			
		||||
			align-items: flex-end;
 | 
			
		||||
			margin-left: auto;
 | 
			
		||||
			padding-left: 10px;
 | 
			
		||||
			overflow: clip;
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.name {
 | 
			
		||||
	flex-shrink: 1;
 | 
			
		||||
	display: block;
 | 
			
		||||
	// note, these margin top values were done by hand may need futher checking if it actualy aligns pixel perfect
 | 
			
		||||
	margin: 3px .5em 0 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	overflow: scroll;
 | 
			
		||||
	overflow-wrap: anywhere;
 | 
			
		||||
	font-size: 1em;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	max-width: 300px;
 | 
			
		||||
 | 
			
		||||
		&::-webkit-scrollbar {
 | 
			
		||||
			display: none;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:hover {
 | 
			
		||||
			color: var(--nameHover);
 | 
			
		||||
			text-decoration: none;
 | 
			
		||||
		}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicName {
 | 
			
		||||
	flex-shrink: 1;
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	font-size: 1em;
 | 
			
		||||
	font-weight: bold;
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		text-decoration: underline;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.isBot {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	align-self: center;
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
	padding: 1px 6px;
 | 
			
		||||
	font-size: 80%;
 | 
			
		||||
	border: solid 0.5px var(--divider);
 | 
			
		||||
	border-radius: var(--radius-xs);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.username {
 | 
			
		||||
	flex-shrink: 9999999;
 | 
			
		||||
	// note these top margins were made to align with the instance ticker
 | 
			
		||||
	margin: 4px .5em 0 0;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
	font-size: .95em;
 | 
			
		||||
	max-width: 300px;
 | 
			
		||||
 | 
			
		||||
	&::-webkit-scrollbar {
 | 
			
		||||
		display: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicUsername {
 | 
			
		||||
	flex-shrink: 9999999;
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
	text-overflow: ellipsis;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.info {
 | 
			
		||||
	&:first-child {
 | 
			
		||||
		margin-top: 4px;
 | 
			
		||||
		flex-shrink: 0;
 | 
			
		||||
		margin-left: auto;
 | 
			
		||||
		font-size: 0.9em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:not(:first-child) {
 | 
			
		||||
		flex-shrink: 0;
 | 
			
		||||
		margin-left: auto;
 | 
			
		||||
		font-size: 0.9em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.classicInfo {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	margin-left: auto;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.time {
 | 
			
		||||
	text-decoration: none;
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		text-decoration: none;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badgeRoles {
 | 
			
		||||
	margin: 0 .5em 0 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.badgeRole {
 | 
			
		||||
	height: 1.3em;
 | 
			
		||||
	vertical-align: -20%;
 | 
			
		||||
 | 
			
		||||
	& + .badgeRole {
 | 
			
		||||
		margin-left: 0.2em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.danger {
 | 
			
		||||
		color: var(--accent);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@container (max-width: 500px) {
 | 
			
		||||
		.name, .username {
 | 
			
		||||
			max-width: 200px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										113
									
								
								packages/frontend/src/components/SkNoteSimple.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										113
									
								
								packages/frontend/src/components/SkNoteSimple.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,113 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
 | 
			
		||||
	<div :class="$style.main">
 | 
			
		||||
		<MkNoteHeader :class="$style.header" :classic="true" :note="note" :mini="true"/>
 | 
			
		||||
		<div>
 | 
			
		||||
			<p v-if="note.cw != null" :class="$style.cw">
 | 
			
		||||
				<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'" :emojiUrls="note.emojis"/>
 | 
			
		||||
				<MkCwButton v-model="showContent" :note="note" v-on:click.stop/>
 | 
			
		||||
			</p>
 | 
			
		||||
			<div v-show="note.cw == null || showContent">
 | 
			
		||||
				<MkSubNoteContent :hideFiles="hideFiles" :class="$style.text" :note="note"/>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { watch } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import MkNoteHeader from '@/components/MkNoteHeader.vue';
 | 
			
		||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
 | 
			
		||||
import MkCwButton from '@/components/MkCwButton.vue';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	expandAllCws?: boolean;
 | 
			
		||||
	hideFiles?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
let showContent = $ref(false);
 | 
			
		||||
 | 
			
		||||
watch(() => props.expandAllCws, (expandAllCws) => {
 | 
			
		||||
	if (expandAllCws !== showContent) showContent = expandAllCws;
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	display: flex;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	font-size: 0.95em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.avatar {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 10px 0 0;
 | 
			
		||||
	width: 34px;
 | 
			
		||||
	height: 34px;
 | 
			
		||||
	border-radius: var(--radius-sm);
 | 
			
		||||
	position: sticky !important;
 | 
			
		||||
	top: calc(16px + var(--stickyTop, 0px));
 | 
			
		||||
	left: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
	flex: 1;
 | 
			
		||||
	min-width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
	margin-bottom: 2px;
 | 
			
		||||
	z-index: 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cw {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	overflow-wrap: break-word;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	cursor: default;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (min-width: 250px) {
 | 
			
		||||
	.avatar {
 | 
			
		||||
		margin: 0 10px 0 0;
 | 
			
		||||
		width: 40px;
 | 
			
		||||
		height: 40px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (min-width: 350px) {
 | 
			
		||||
	.avatar {
 | 
			
		||||
		margin: 0 10px 0 0;
 | 
			
		||||
		width: 44px;
 | 
			
		||||
		height: 44px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (min-width: 500px) {
 | 
			
		||||
	.avatar {
 | 
			
		||||
		margin: 0 12px 0 0;
 | 
			
		||||
		width: 48px;
 | 
			
		||||
		height: 48px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										545
									
								
								packages/frontend/src/components/SkNoteSub.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										545
									
								
								packages/frontend/src/components/SkNoteSub.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,545 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div v-if="!muted" ref="el" :class="[$style.root, { [$style.children]: depth > 1 }]">
 | 
			
		||||
	<div v-if="!hideLine" :class="$style.line"></div>
 | 
			
		||||
	<div :class="$style.main">
 | 
			
		||||
		<div v-if="note.channel" :class="$style.colorBar" :style="{ background: note.channel.color }"></div>
 | 
			
		||||
		<MkAvatar :class="$style.avatar" :user="note.user" link preview/>
 | 
			
		||||
		<div :class="$style.body">
 | 
			
		||||
			<SkNoteHeader :class="$style.header" :note="note" :classic="true" :mini="true"/>
 | 
			
		||||
			<div :class="$style.content">
 | 
			
		||||
				<p v-if="note.cw != null" :class="$style.cw">
 | 
			
		||||
					<Mfm v-if="note.cw != ''" style="margin-right: 8px;" :text="note.cw" :author="note.user" :nyaize="'respect'"/>
 | 
			
		||||
					<MkCwButton v-model="showContent" :note="note"/>
 | 
			
		||||
				</p>
 | 
			
		||||
				<div v-show="note.cw == null || showContent">
 | 
			
		||||
					<MkSubNoteContent :class="$style.text" :note="note" :translating="translating" :translation="translation"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<footer :class="$style.footer">
 | 
			
		||||
				<MkReactionsViewer ref="reactionsViewer" :note="note"/>
 | 
			
		||||
				<button class="_button" :class="$style.noteFooterButton" @click="reply()">
 | 
			
		||||
					<i class="ph-arrow-u-up-left ph-bold ph-lg"></i>
 | 
			
		||||
					<p v-if="note.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ note.repliesCount }}</p>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button
 | 
			
		||||
					v-if="canRenote"
 | 
			
		||||
					ref="renoteButton"
 | 
			
		||||
					class="_button"
 | 
			
		||||
					:class="$style.noteFooterButton"
 | 
			
		||||
					:style="renoted ? 'color: var(--accent) !important;' : ''"
 | 
			
		||||
					@mousedown="renoted ? undoRenote() : renote()"
 | 
			
		||||
				>
 | 
			
		||||
					<i class="ph-rocket-launch ph-bold ph-lg"></i>
 | 
			
		||||
					<p v-if="note.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ note.renoteCount }}</p>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button
 | 
			
		||||
					v-if="canRenote"
 | 
			
		||||
					ref="quoteButton"
 | 
			
		||||
					class="_button"
 | 
			
		||||
					:class="$style.noteFooterButton"
 | 
			
		||||
					@mousedown="quote()"
 | 
			
		||||
				>
 | 
			
		||||
					<i class="ph-quotes ph-bold ph-lg"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button v-else class="_button" :class="$style.noteFooterButton" disabled>
 | 
			
		||||
					<i class="ph-prohibit ph-bold ph-lg"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button v-if="note.myReaction == null && note.reactionAcceptance !== 'likeOnly'" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
 | 
			
		||||
					<i class="ph-heart ph-bold ph-lg"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button v-if="note.myReaction == null" ref="reactButton" :class="$style.noteFooterButton" class="_button" @mousedown="react()">
 | 
			
		||||
					<i v-if="note.reactionAcceptance === 'likeOnly'" class="ph-heart ph-bold ph-lg"></i>
 | 
			
		||||
					<i v-else class="ph-smiley ph-bold ph-lg"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button v-if="note.myReaction != null" ref="reactButton" class="_button" :class="[$style.noteFooterButton, $style.reacted]" @click="undoReact(note)">
 | 
			
		||||
					<i class="ph-minus ph-bold ph-lg"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
				<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown="menu()">
 | 
			
		||||
					<i class="ph-dots-three ph-bold ph-lg"></i>
 | 
			
		||||
				</button>
 | 
			
		||||
			</footer>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
	<template v-if="depth < 5">
 | 
			
		||||
		<MkNoteSub v-for="reply in replies" :key="reply.id" :note="reply" :class="$style.reply" :detail="true" :depth="depth + 1" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	</template>
 | 
			
		||||
	<div v-else :class="$style.more">
 | 
			
		||||
		<MkA class="_link" :to="notePage(note)">{{ i18n.ts.continueThread }} <i class="ph-caret-double-right ph-bold ph-lg"></i></MkA>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
<div v-else :class="$style.muted" @click="muted = false">
 | 
			
		||||
	<I18n :src="i18n.ts.userSaysSomething" tag="small">
 | 
			
		||||
		<template #name>
 | 
			
		||||
			<MkA v-user-preview="note.userId" :to="userPage(note.user)">
 | 
			
		||||
				<MkUserName :user="note.user"/>
 | 
			
		||||
			</MkA>
 | 
			
		||||
		</template>
 | 
			
		||||
	</I18n>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, ref, shallowRef, watch } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import SkNoteHeader from '@/components/SkNoteHeader.vue';
 | 
			
		||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
 | 
			
		||||
import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
 | 
			
		||||
import MkCwButton from '@/components/MkCwButton.vue';
 | 
			
		||||
import { notePage } from '@/filters/note.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { userPage } from "@/filters/user.js";
 | 
			
		||||
import { checkWordMute } from "@/scripts/check-word-mute.js";
 | 
			
		||||
import { defaultStore } from "@/store.js";
 | 
			
		||||
import { pleaseLogin } from '@/scripts/please-login.js';
 | 
			
		||||
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
 | 
			
		||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
 | 
			
		||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
 | 
			
		||||
import { claimAchievement } from '@/scripts/achievements.js';
 | 
			
		||||
import type { MenuItem } from '@/types/menu.js';
 | 
			
		||||
import { getNoteMenu } from '@/scripts/get-note-menu.js';
 | 
			
		||||
import { useNoteCapture } from '@/scripts/use-note-capture.js';
 | 
			
		||||
 | 
			
		||||
const canRenote = computed(() => ['public', 'home'].includes(props.note.visibility) || props.note.userId === $i.id);
 | 
			
		||||
const hideLine = computed(() => { return props.detail ? true : false; });
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
	detail?: boolean;
 | 
			
		||||
	expandAllCws?: boolean;
 | 
			
		||||
 | 
			
		||||
	// how many notes are in between this one and the note being viewed in detail
 | 
			
		||||
	depth?: number;
 | 
			
		||||
}>(), {
 | 
			
		||||
	depth: 1,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const el = shallowRef<HTMLElement>();
 | 
			
		||||
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
 | 
			
		||||
const translation = ref<any>(null);
 | 
			
		||||
const translating = ref(false);
 | 
			
		||||
const isDeleted = ref(false);
 | 
			
		||||
const renoted = ref(false);
 | 
			
		||||
const reactButton = shallowRef<HTMLElement>();
 | 
			
		||||
const renoteButton = shallowRef<HTMLElement>();
 | 
			
		||||
const quoteButton = shallowRef<HTMLElement>();
 | 
			
		||||
const menuButton = shallowRef<HTMLElement>();
 | 
			
		||||
const likeButton = shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
let appearNote = $computed(() => isRenote ? props.note.renote as Misskey.entities.Note : props.note);
 | 
			
		||||
const defaultLike = computed(() => defaultStore.state.like ? defaultStore.state.like : null);
 | 
			
		||||
 | 
			
		||||
const isRenote = (
 | 
			
		||||
	props.note.renote != null &&
 | 
			
		||||
	props.note.text == null &&
 | 
			
		||||
	props.note.fileIds.length === 0 &&
 | 
			
		||||
	props.note.poll == null
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
useNoteCapture({
 | 
			
		||||
	rootEl: el,
 | 
			
		||||
	note: $$(appearNote),
 | 
			
		||||
	isDeletedRef: isDeleted,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
if ($i) {
 | 
			
		||||
	os.api("notes/renotes", {
 | 
			
		||||
		noteId: appearNote.id,
 | 
			
		||||
		userId: $i.id,
 | 
			
		||||
		limit: 1,
 | 
			
		||||
	}).then((res) => {
 | 
			
		||||
		renoted.value = res.length > 0;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function focus() {
 | 
			
		||||
	el.value.focus();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function reply(viaKeyboard = false): void {
 | 
			
		||||
	pleaseLogin();
 | 
			
		||||
	showMovedDialog();
 | 
			
		||||
	os.post({
 | 
			
		||||
		reply: props.note,
 | 
			
		||||
		channel: props.note.channel,
 | 
			
		||||
		animation: !viaKeyboard,
 | 
			
		||||
	}, () => {
 | 
			
		||||
		focus();
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function react(viaKeyboard = false): void {
 | 
			
		||||
	pleaseLogin();
 | 
			
		||||
	showMovedDialog();
 | 
			
		||||
	if (props.note.reactionAcceptance === 'likeOnly') {
 | 
			
		||||
		os.api('notes/like', {
 | 
			
		||||
			noteId: props.note.id,
 | 
			
		||||
			override: defaultLike.value,
 | 
			
		||||
		});
 | 
			
		||||
		const el = reactButton.value as HTMLElement | null | undefined;
 | 
			
		||||
		if (el) {
 | 
			
		||||
			const rect = el.getBoundingClientRect();
 | 
			
		||||
			const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
			const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
		}
 | 
			
		||||
	} else {
 | 
			
		||||
		blur();
 | 
			
		||||
		reactionPicker.show(reactButton.value, reaction => {
 | 
			
		||||
			os.api('notes/reactions/create', {
 | 
			
		||||
				noteId: props.note.id,
 | 
			
		||||
				reaction: reaction,
 | 
			
		||||
			});
 | 
			
		||||
			if (props.note.text && props.note.text.length > 100 && (Date.now() - new Date(props.note.createdAt).getTime() < 1000 * 3)) {
 | 
			
		||||
				claimAchievement('reactWithoutRead');
 | 
			
		||||
			}
 | 
			
		||||
		}, () => {
 | 
			
		||||
			focus();
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function like(): void {
 | 
			
		||||
	pleaseLogin();
 | 
			
		||||
	showMovedDialog();
 | 
			
		||||
	os.api('notes/like', {
 | 
			
		||||
		noteId: props.note.id,
 | 
			
		||||
		override: defaultLike.value,
 | 
			
		||||
	});
 | 
			
		||||
	const el = reactButton.value as HTMLElement | null | undefined;
 | 
			
		||||
	if (el) {
 | 
			
		||||
		const rect = el.getBoundingClientRect();
 | 
			
		||||
		const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
		const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
		os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function undoReact(note): void {
 | 
			
		||||
	const oldReaction = note.myReaction;
 | 
			
		||||
	if (!oldReaction) return;
 | 
			
		||||
	os.api('notes/reactions/delete', {
 | 
			
		||||
		noteId: note.id,
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function undoRenote() : void {
 | 
			
		||||
	if (!renoted.value) return;
 | 
			
		||||
	os.api("notes/unrenote", {
 | 
			
		||||
		noteId: appearNote.id,
 | 
			
		||||
	});
 | 
			
		||||
	os.toast(i18n.ts.rmboost);
 | 
			
		||||
	renoted.value = false;
 | 
			
		||||
 | 
			
		||||
	const el = renoteButton.value as HTMLElement | null | undefined;
 | 
			
		||||
	if (el) {
 | 
			
		||||
		const rect = el.getBoundingClientRect();
 | 
			
		||||
		const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
		const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
		os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let showContent = $ref(false);
 | 
			
		||||
 | 
			
		||||
watch(() => props.expandAllCws, (expandAllCws) => {
 | 
			
		||||
	if (expandAllCws !== showContent) showContent = expandAllCws;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let replies: Misskey.entities.Note[] = $ref([]);
 | 
			
		||||
 | 
			
		||||
function renote() {
 | 
			
		||||
	pleaseLogin();
 | 
			
		||||
	showMovedDialog();
 | 
			
		||||
 | 
			
		||||
	if (appearNote.channel) {
 | 
			
		||||
		const el = renoteButton.value as HTMLElement | null | undefined;
 | 
			
		||||
		if (el) {
 | 
			
		||||
			const rect = el.getBoundingClientRect();
 | 
			
		||||
			const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
			const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		os.api('notes/create', {
 | 
			
		||||
			renoteId: props.note.id,
 | 
			
		||||
			channelId: props.note.channelId,
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			os.toast(i18n.ts.renoted);
 | 
			
		||||
			renoted.value = true;
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		const el = renoteButton.value as HTMLElement | null | undefined;
 | 
			
		||||
		if (el) {
 | 
			
		||||
			const rect = el.getBoundingClientRect();
 | 
			
		||||
			const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
			const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
			os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		os.api('notes/create', {
 | 
			
		||||
			renoteId: props.note.id,
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			os.toast(i18n.ts.renoted);
 | 
			
		||||
			renoted.value = true;
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function quote() {
 | 
			
		||||
	pleaseLogin();
 | 
			
		||||
	showMovedDialog();
 | 
			
		||||
 | 
			
		||||
	if (appearNote.channel) {
 | 
			
		||||
		os.post({
 | 
			
		||||
			renote: appearNote,
 | 
			
		||||
			channel: appearNote.channel,
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			os.api("notes/renotes", {
 | 
			
		||||
				noteId: props.note.id,
 | 
			
		||||
				userId: $i.id,
 | 
			
		||||
				limit: 1,
 | 
			
		||||
				quote: true,
 | 
			
		||||
			}).then((res) => {
 | 
			
		||||
				if (!(res.length > 0)) return;
 | 
			
		||||
				const el = quoteButton.value as HTMLElement | null | undefined;
 | 
			
		||||
				if (el && res.length > 0) {
 | 
			
		||||
					const rect = el.getBoundingClientRect();
 | 
			
		||||
					const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
					const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
					os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				os.toast(i18n.ts.quoted);
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		os.post({
 | 
			
		||||
			renote: appearNote,
 | 
			
		||||
		}).then(() => {
 | 
			
		||||
			os.api("notes/renotes", {
 | 
			
		||||
				noteId: props.note.id,
 | 
			
		||||
				userId: $i.id,
 | 
			
		||||
				limit: 1,
 | 
			
		||||
				quote: true,
 | 
			
		||||
			}).then((res) => {
 | 
			
		||||
				if (!(res.length > 0)) return;
 | 
			
		||||
				const el = quoteButton.value as HTMLElement | null | undefined;
 | 
			
		||||
				if (el && res.length > 0) {
 | 
			
		||||
					const rect = el.getBoundingClientRect();
 | 
			
		||||
					const x = rect.left + (el.offsetWidth / 2);
 | 
			
		||||
					const y = rect.top + (el.offsetHeight / 2);
 | 
			
		||||
					os.popup(MkRippleEffect, { x, y }, {}, 'end');
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				os.toast(i18n.ts.quoted);
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function menu(viaKeyboard = false): void {
 | 
			
		||||
	const { menu, cleanup } = getNoteMenu({ note: props.note, translating, translation, menuButton, isDeleted });
 | 
			
		||||
	os.popupMenu(menu, menuButton.value, {
 | 
			
		||||
		viaKeyboard,
 | 
			
		||||
	}).then(focus).finally(cleanup);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (props.detail) {
 | 
			
		||||
	os.api('notes/children', {
 | 
			
		||||
		noteId: props.note.id,
 | 
			
		||||
		limit: 5,
 | 
			
		||||
	}).then(res => {
 | 
			
		||||
		replies = res;
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	padding: 28px 32px;
 | 
			
		||||
	font-size: 0.9em;
 | 
			
		||||
	position: relative;
 | 
			
		||||
 | 
			
		||||
	&.children {
 | 
			
		||||
		padding: 10px 0 0 16px;
 | 
			
		||||
		font-size: 1em;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.line {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	left: 60px;
 | 
			
		||||
	// using solid instead of dotted, stylelistic choice
 | 
			
		||||
	border-left: 2.5px solid rgb(174, 174, 174);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.footer {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	z-index: 1;
 | 
			
		||||
	margin-top: 0.4em;
 | 
			
		||||
	width: max-content;
 | 
			
		||||
	min-width: max-content;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
	display: flex;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.colorBar {
 | 
			
		||||
	position: absolute;
 | 
			
		||||
	top: 8px;
 | 
			
		||||
	left: 8px;
 | 
			
		||||
	width: 5px;
 | 
			
		||||
	height: calc(100% - 8px);
 | 
			
		||||
	border-radius: var(--radius-ellipse);
 | 
			
		||||
	pointer-events: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.avatar {
 | 
			
		||||
	flex-shrink: 0;
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0 14px 0 0;
 | 
			
		||||
	width: 58px;
 | 
			
		||||
	height: 58px;
 | 
			
		||||
	border-radius: var(--radius-sm);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.body {
 | 
			
		||||
	flex: 1;
 | 
			
		||||
	min-width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content {
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header {
 | 
			
		||||
	margin-bottom: 2px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.noteFooterButton {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 8px;
 | 
			
		||||
	padding-top: 10px;
 | 
			
		||||
	opacity: 0.7;
 | 
			
		||||
 | 
			
		||||
	&:not(:last-child) {
 | 
			
		||||
		margin-right: 1.5em;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:hover {
 | 
			
		||||
		color: var(--fgHighlighted);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reply, .more {
 | 
			
		||||
	border-left: solid 0.5px var(--divider);
 | 
			
		||||
	margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more {
 | 
			
		||||
	padding: 10px 0 0 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 580px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: 28px 26px 0;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.line {
 | 
			
		||||
		left: 50.5px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.avatar {
 | 
			
		||||
		width: 50px;
 | 
			
		||||
		height: 50px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 500px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: 23px 25px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 400px) {
 | 
			
		||||
	.noteFooterButton {
 | 
			
		||||
		&:not(:last-child) {
 | 
			
		||||
			margin-right: 0.7em;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.noteFooterButtonCount {
 | 
			
		||||
	display: inline;
 | 
			
		||||
	margin: 0 0 0 8px;
 | 
			
		||||
	opacity: 0.7;
 | 
			
		||||
 | 
			
		||||
	&.reacted {
 | 
			
		||||
		color: var(--accent);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.cw {
 | 
			
		||||
	display: block;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
	overflow-wrap: break-word;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	padding: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reply, .more {
 | 
			
		||||
	border-left: solid 0.5px var(--divider);
 | 
			
		||||
	margin-top: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.more {
 | 
			
		||||
	padding: 10px 0 0 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 480px) {
 | 
			
		||||
	.root {
 | 
			
		||||
		padding: 22px 24px;
 | 
			
		||||
 | 
			
		||||
		&.children {
 | 
			
		||||
			padding: 10px 0 0 8px;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@container (max-width: 450px) {
 | 
			
		||||
	.line {
 | 
			
		||||
		left: 46px;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	.avatar {
 | 
			
		||||
		width: 46px;
 | 
			
		||||
		height: 46px;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.muted {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	padding: 8px !important;
 | 
			
		||||
	border: 1px solid var(--divider);
 | 
			
		||||
	margin: 8px 8px 0 8px;
 | 
			
		||||
	border-radius: var(--radius-sm);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -16,10 +16,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
 | 
			
		||||
					<div class="_margin">
 | 
			
		||||
						<MkButton v-if="!showNext" :class="$style.loadNext" @click="showNext = true"><i class="ph-caret-up ph-bold ph-lg"></i></MkButton>
 | 
			
		||||
						<div class="_margin _gaps_s">
 | 
			
		||||
						<div v-if="defaultStore.state.noteDesign === 'misskey'" class="_margin _gaps_s">
 | 
			
		||||
							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
 | 
			
		||||
							<MkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note" :expandAllCws="expandAllCws"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-else-if="defaultStore.state.noteDesign === 'sharkey'" class="_margin _gaps_s">
 | 
			
		||||
							<MkRemoteCaution v-if="note.user.host != null" :href="note.url ?? note.uri"/>
 | 
			
		||||
							<SkNoteDetailed :key="note.id" v-model:note="note" :class="$style.note" :expandAllCws="expandAllCws"/>
 | 
			
		||||
						</div>
 | 
			
		||||
						<div v-if="clips && clips.length > 0" class="_margin">
 | 
			
		||||
							<div style="font-weight: bold; padding: 12px;">{{ i18n.ts.clip }}</div>
 | 
			
		||||
							<div class="_gaps">
 | 
			
		||||
| 
						 | 
				
			
			@ -48,6 +52,7 @@ import { computed, watch } from 'vue';
 | 
			
		|||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 | 
			
		||||
import MkNotes from '@/components/MkNotes.vue';
 | 
			
		||||
import SkNoteDetailed from '@/components/SkNoteDetailed.vue';
 | 
			
		||||
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -59,6 +59,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
					<option value="medium">{{ i18n.ts.medium }}</option>
 | 
			
		||||
					<option value="large">{{ i18n.ts.large }}</option>
 | 
			
		||||
				</MkRadios>
 | 
			
		||||
				<MkRadios v-model="noteDesign">
 | 
			
		||||
					<template #label>Note Design</template>
 | 
			
		||||
					<option value="sharkey">Sharkey</option>
 | 
			
		||||
					<option value="misskey">Misskey</option>
 | 
			
		||||
			</MkRadios>
 | 
			
		||||
			</div>
 | 
			
		||||
 | 
			
		||||
			<MkSelect v-model="instanceTicker">
 | 
			
		||||
| 
						 | 
				
			
			@ -273,6 +278,7 @@ const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
 | 
			
		|||
const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
 | 
			
		||||
const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
 | 
			
		||||
const showTickerOnReplies = computed(defaultStore.makeGetterSetter('showTickerOnReplies'));
 | 
			
		||||
const noteDesign = computed(defaultStore.makeGetterSetter('noteDesign'));
 | 
			
		||||
 | 
			
		||||
watch(lang, () => {
 | 
			
		||||
	miLocalStorage.setItem('lang', lang.value as string);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -254,6 +254,10 @@ export const defaultStore = markRaw(new Storage('base', {
 | 
			
		|||
		where: 'device',
 | 
			
		||||
		default: false,
 | 
			
		||||
	},
 | 
			
		||||
	noteDesign: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
		default: 'sharkey' as 'sharkey' | 'misskey',
 | 
			
		||||
	},
 | 
			
		||||
	enableInfiniteScroll: {
 | 
			
		||||
		where: 'device',
 | 
			
		||||
		default: true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		
		Reference in a new issue