feat: Note Highlights

Reviewed-on: https://git.joinsharkey.org/Sharkey/Sharkey/pulls/304
This commit is contained in:
Amelia Yukii 2024-01-03 19:46:03 +01:00
commit bf5e62301a
5 changed files with 125 additions and 9 deletions

View file

@ -776,6 +776,10 @@ function focusAfter() {
focusNext(el.value); focusNext(el.value);
} }
function scrollIntoView() {
el.value.scrollIntoView();
}
function readPromo() { function readPromo() {
os.api('promo/read', { os.api('promo/read', {
noteId: appearNote.value.id, noteId: appearNote.value.id,
@ -790,6 +794,12 @@ function emitUpdReaction(emoji: string, delta: number) {
emit('reaction', emoji); emit('reaction', emoji);
} }
} }
defineExpose({
focus,
blur,
scrollIntoView,
});
</script> </script>
<style lang="scss" module> <style lang="scss" module>
@ -824,7 +834,7 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: auto; margin: auto;
width: calc(100% - 8px); width: calc(100% - 8px);
height: calc(100% - 8px); height: calc(100% - 8px);
border: dashed 1px var(--focus); border: solid 1px var(--focus);
border-radius: var(--radius); border-radius: var(--radius);
box-sizing: border-box; box-sizing: border-box;
} }
@ -894,7 +904,7 @@ function emitUpdReaction(emoji: string, delta: number) {
position: relative; position: relative;
display: flex; display: flex;
align-items: center; align-items: center;
padding: 24px 32px 16px calc(32px + var(--avatar) + 14px); padding: 24px 32px 0 calc(32px + var(--avatar) + 14px);
line-height: 28px; line-height: 28px;
white-space: pre; white-space: pre;
color: var(--renote); color: var(--renote);

View file

@ -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"/> <SkNoteSub v-for="note in conversation" :key="note.id" :class="$style.replyToMore" :note="note" :expandAllCws="props.expandAllCws"/>
</template> </template>
<SkNoteSub v-if="appearNote.reply" :note="appearNote.reply" :class="$style.replyTo" :expandAllCws="props.expandAllCws"/> <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"> <header :class="$style.noteHeader">
<MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/> <MkAvatar :class="$style.noteHeaderAvatar" :user="appearNote.user" indicator link preview/>
<div style="display: flex; align-items: center; white-space: nowrap; overflow: hidden;"> <div style="display: flex; align-items: center; white-space: nowrap; overflow: hidden;">
@ -228,7 +228,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <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 mfm from '@sharkey/sfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import SkNoteSub from '@/components/SkNoteSub.vue'; import SkNoteSub from '@/components/SkNoteSub.vue';
@ -301,6 +301,7 @@ const isRenote = (
); );
const el = shallowRef<HTMLElement>(); const el = shallowRef<HTMLElement>();
const noteEl = shallowRef<HTMLElement>();
const menuButton = shallowRef<HTMLElement>(); const menuButton = shallowRef<HTMLElement>();
const menuVersionsButton = shallowRef<HTMLElement>(); const menuVersionsButton = shallowRef<HTMLElement>();
const renoteButton = shallowRef<HTMLElement>(); const renoteButton = shallowRef<HTMLElement>();
@ -731,11 +732,11 @@ function showRenoteMenu(viaKeyboard = false): void {
} }
function focus() { function focus() {
el.value.focus(); noteEl.value?.focus();
} }
function blur() { function blur() {
el.value.blur(); noteEl.value?.blur();
} }
const repliesLoaded = ref(false); const repliesLoaded = ref(false);
@ -776,6 +777,7 @@ function loadConversation() {
noteId: appearNote.value.replyId, noteId: appearNote.value.replyId,
}).then(res => { }).then(res => {
conversation.value = res.reverse(); conversation.value = res.reverse();
focus();
}); });
} }
@ -792,6 +794,31 @@ function animatedMFM() {
}).then((res) => { if (!res.canceled) allowAnim.value = true; }); }).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> </script>
<style lang="scss" module> <style lang="scss" module>
@ -863,6 +890,7 @@ function animatedMFM() {
} }
.note { .note {
position: relative;
padding: 32px; padding: 32px;
font-size: 1.2em; font-size: 1.2em;
overflow: hidden; overflow: hidden;
@ -870,6 +898,28 @@ function animatedMFM() {
&:hover > .main > .footer > .button { &:hover > .main > .footer > .button {
opacity: 1; 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 { .noteHeader {

View file

@ -461,7 +461,27 @@ if (props.detail) {
} }
.main { .main {
display: flex; 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 { .colorBar {

View file

@ -78,7 +78,7 @@ export class Router extends EventEmitter<{
public current: Resolved; public current: Resolved;
public currentRef: ShallowRef<Resolved> = shallowRef(); public currentRef: ShallowRef<Resolved> = shallowRef();
public currentRoute: ShallowRef<RouteDef> = shallowRef(); public currentRoute: ShallowRef<RouteDef> = shallowRef();
private currentPath: string; private currentPath = '';
private isLoggedIn: boolean; private isLoggedIn: boolean;
private notFoundPageComponent: Component; private notFoundPageComponent: Component;
private currentKey = Date.now().toString(); private currentKey = Date.now().toString();
@ -89,7 +89,7 @@ export class Router extends EventEmitter<{
super(); super();
this.routes = routes; this.routes = routes;
this.currentPath = currentPath; //this.currentPath = currentPath;
this.isLoggedIn = isLoggedIn; this.isLoggedIn = isLoggedIn;
this.notFoundPageComponent = notFoundPageComponent; this.notFoundPageComponent = notFoundPageComponent;
this.navigate(currentPath, null, false); this.navigate(currentPath, null, false);

View file

@ -545,12 +545,48 @@ export const mainRouter = new Router(routes, location.pathname + location.search
window.history.replaceState({ key: mainRouter.getCurrentKey() }, '', location.href); 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 => { mainRouter.addListener('push', ctx => {
window.history.pushState({ key: ctx.key }, '', ctx.path); 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) => { window.addEventListener('popstate', (event) => {
mainRouter.replace(location.pathname + location.search + location.hash, event.state?.key); 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 { export function useRouter(): Router {