feat: Note Highlights
Reviewed-on: https://git.joinsharkey.org/Sharkey/Sharkey/pulls/304
This commit is contained in:
		
						commit
						bf5e62301a
					
				
					 5 changed files with 125 additions and 9 deletions
				
			
		| 
						 | 
				
			
			@ -776,6 +776,10 @@ function focusAfter() {
 | 
			
		|||
	focusNext(el.value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function scrollIntoView() {
 | 
			
		||||
	el.value.scrollIntoView();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function readPromo() {
 | 
			
		||||
	os.api('promo/read', {
 | 
			
		||||
		noteId: appearNote.value.id,
 | 
			
		||||
| 
						 | 
				
			
			@ -790,6 +794,12 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
		emit('reaction', emoji);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
defineExpose({
 | 
			
		||||
	focus,
 | 
			
		||||
	blur,
 | 
			
		||||
	scrollIntoView,
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
| 
						 | 
				
			
			@ -824,7 +834,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
			margin: auto;
 | 
			
		||||
			width: calc(100% - 8px);
 | 
			
		||||
			height: calc(100% - 8px);
 | 
			
		||||
			border: dashed 1px var(--focus);
 | 
			
		||||
			border: solid 1px var(--focus);
 | 
			
		||||
			border-radius: var(--radius);
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
		}
 | 
			
		||||
| 
						 | 
				
			
			@ -894,7 +904,7 @@ function emitUpdReaction(emoji: string, delta: number) {
 | 
			
		|||
	position: relative;
 | 
			
		||||
	display: flex;
 | 
			
		||||
	align-items: center;
 | 
			
		||||
	padding: 24px 32px 16px calc(32px + var(--avatar) + 14px);
 | 
			
		||||
	padding: 24px 32px 0 calc(32px + var(--avatar) + 14px);
 | 
			
		||||
	line-height: 28px;
 | 
			
		||||
	white-space: pre;
 | 
			
		||||
	color: var(--renote);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
		<SkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	</template>
 | 
			
		||||
	<SkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/>
 | 
			
		||||
	<article :class="$style.note" @contextmenu.stop="onContextmenu">
 | 
			
		||||
	<article :id="appearNote.id" ref="noteEl" :class="$style.note" tabindex="-1" @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;">
 | 
			
		||||
| 
						 | 
				
			
			@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { computed, inject, onMounted, provide, ref, shallowRef, watch } from 'vue';
 | 
			
		||||
import { computed, inject, onMounted, onUnmounted, onUpdated, provide, ref, shallowRef, watch } from 'vue';
 | 
			
		||||
import * as mfm from '@sharkey/sfm-js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import SkNoteSub from '@/components/SkNoteSub.vue';
 | 
			
		||||
| 
						 | 
				
			
			@ -301,6 +301,7 @@ const isRenote = (
 | 
			
		|||
);
 | 
			
		||||
 | 
			
		||||
const el = shallowRef<HTMLElement>();
 | 
			
		||||
const noteEl = shallowRef<HTMLElement>();
 | 
			
		||||
const menuButton = shallowRef<HTMLElement>();
 | 
			
		||||
const menuVersionsButton = shallowRef<HTMLElement>();
 | 
			
		||||
const renoteButton = shallowRef<HTMLElement>();
 | 
			
		||||
| 
						 | 
				
			
			@ -731,11 +732,11 @@ function showRenoteMenu(viaKeyboard = false): void {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
function focus() {
 | 
			
		||||
	el.value.focus();
 | 
			
		||||
	noteEl.value?.focus();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function blur() {
 | 
			
		||||
	el.value.blur();
 | 
			
		||||
	noteEl.value?.blur();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const repliesLoaded = ref(false);
 | 
			
		||||
| 
						 | 
				
			
			@ -776,6 +777,7 @@ function loadConversation() {
 | 
			
		|||
		noteId: appearNote.value.replyId,
 | 
			
		||||
	}).then(res => {
 | 
			
		||||
		conversation.value = res.reverse();
 | 
			
		||||
		focus();
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -792,6 +794,31 @@ function animatedMFM() {
 | 
			
		|||
		}).then((res) => { if (!res.canceled) allowAnim.value = true; });
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let isScrolling = false;
 | 
			
		||||
 | 
			
		||||
function setScrolling() {
 | 
			
		||||
	isScrolling = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	document.addEventListener('wheel', setScrolling);
 | 
			
		||||
	isScrolling = false;
 | 
			
		||||
	noteEl.value?.scrollIntoView({ block: 'center' });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUpdated(() => {
 | 
			
		||||
	if (!isScrolling) {
 | 
			
		||||
		noteEl.value?.scrollIntoView({ block: 'center' });
 | 
			
		||||
		if (location.hash) {
 | 
			
		||||
			location.replace(location.hash); // Jump to highlighted reply
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUnmounted(() => {
 | 
			
		||||
	document.removeEventListener('wheel', setScrolling);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
| 
						 | 
				
			
			@ -863,6 +890,7 @@ function animatedMFM() {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.note {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	padding: 32px;
 | 
			
		||||
	font-size: 1.2em;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
| 
						 | 
				
			
			@ -870,6 +898,28 @@ function animatedMFM() {
 | 
			
		|||
	&:hover > .main > .footer > .button {
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&:focus-visible {
 | 
			
		||||
		outline: none;
 | 
			
		||||
 | 
			
		||||
		&:after {
 | 
			
		||||
			content: "";
 | 
			
		||||
			pointer-events: none;
 | 
			
		||||
			display: block;
 | 
			
		||||
			position: absolute;
 | 
			
		||||
			z-index: 10;
 | 
			
		||||
			top: 0;
 | 
			
		||||
			left: 0;
 | 
			
		||||
			right: 0;
 | 
			
		||||
			bottom: 0;
 | 
			
		||||
			margin: auto;
 | 
			
		||||
			width: calc(100% - 8px);
 | 
			
		||||
			height: calc(100% - 8px);
 | 
			
		||||
			border: solid 1px var(--focus);
 | 
			
		||||
			border-radius: var(--radius);
 | 
			
		||||
			box-sizing: border-box;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.noteHeader {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -461,7 +461,27 @@ if (props.detail) {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
.main {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	display:  flex;
 | 
			
		||||
 | 
			
		||||
	&::after {
 | 
			
		||||
		content: "";
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		top: -12px;
 | 
			
		||||
		right: -12px;
 | 
			
		||||
		left: -12px;
 | 
			
		||||
		bottom: -12px;
 | 
			
		||||
		background: var(--panelHighlight);
 | 
			
		||||
		border-radius: var(--radius);
 | 
			
		||||
		opacity: 0;
 | 
			
		||||
		transition: opacity .2s, background .2s;
 | 
			
		||||
		z-index: -1;
 | 
			
		||||
	}
 | 
			
		||||
	
 | 
			
		||||
	&:hover::after,
 | 
			
		||||
	&:focus-within::after {
 | 
			
		||||
		opacity: 1;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.colorBar {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -78,7 +78,7 @@ export class Router extends EventEmitter<{
 | 
			
		|||
	public current: Resolved;
 | 
			
		||||
	public currentRef: ShallowRef<Resolved> = shallowRef();
 | 
			
		||||
	public currentRoute: ShallowRef<RouteDef> = shallowRef();
 | 
			
		||||
	private currentPath: string;
 | 
			
		||||
	private currentPath = '';
 | 
			
		||||
	private isLoggedIn: boolean;
 | 
			
		||||
	private notFoundPageComponent: Component;
 | 
			
		||||
	private currentKey = Date.now().toString();
 | 
			
		||||
| 
						 | 
				
			
			@ -89,7 +89,7 @@ export class Router extends EventEmitter<{
 | 
			
		|||
		super();
 | 
			
		||||
 | 
			
		||||
		this.routes = routes;
 | 
			
		||||
		this.currentPath = currentPath;
 | 
			
		||||
		//this.currentPath = currentPath;
 | 
			
		||||
		this.isLoggedIn = isLoggedIn;
 | 
			
		||||
		this.notFoundPageComponent = notFoundPageComponent;
 | 
			
		||||
		this.navigate(currentPath, null, false);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -545,12 +545,48 @@ export const mainRouter = new Router(routes, location.pathname + location.search
 | 
			
		|||
 | 
			
		||||
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href);
 | 
			
		||||
 | 
			
		||||
const scrollPosStore = new Map<string, number>();
 | 
			
		||||
let restoring = false;
 | 
			
		||||
 | 
			
		||||
window.setInterval(() => {
 | 
			
		||||
	if (!restoring) {
 | 
			
		||||
		scrollPosStore.set(window.history.state?.key, window.scrollY);
 | 
			
		||||
	}
 | 
			
		||||
}, 1000);
 | 
			
		||||
 | 
			
		||||
mainRouter.addListener('push', ctx => {
 | 
			
		||||
	window.history.pushState({ key: ctx.key }, '', ctx.path);
 | 
			
		||||
 | 
			
		||||
	restoring = true;
 | 
			
		||||
	const scrollPos = scrollPosStore.get(ctx.key) ?? 0;
 | 
			
		||||
	window.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
 | 
			
		||||
	if (scrollPos !== 0) {
 | 
			
		||||
		window.setTimeout(() => {
 | 
			
		||||
			// 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
 | 
			
		||||
			window.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
		}, 100);
 | 
			
		||||
		restoring = false;
 | 
			
		||||
	} else {
 | 
			
		||||
		restoring = false;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
mainRouter.addListener('same', () => {
 | 
			
		||||
	window.scroll({ top: 0, behavior: 'smooth' });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
window.addEventListener('popstate', (event) => {
 | 
			
		||||
	mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key);
 | 
			
		||||
 | 
			
		||||
	restoring = true;
 | 
			
		||||
	const scrollPos = scrollPosStore.get(event.state?.key) ?? 0;
 | 
			
		||||
	window.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
	window.setTimeout(() => {
 | 
			
		||||
		// 遷移直後はタイミングによってはコンポーネントが復元し切ってない可能性も考えられるため少し時間を空けて再度スクロール
 | 
			
		||||
		window.scroll({ top: scrollPos, behavior: 'instant' });
 | 
			
		||||
		restoring = false;
 | 
			
		||||
	}, 100);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export function useRouter(): Router {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue