Skip to content

Commit

Permalink
[frontend] Mobile UX (reduce margin, hide on scroll)
Browse files Browse the repository at this point in the history
  • Loading branch information
yuriha-chan committed Nov 9, 2024
1 parent 54259eb commit 06e2d70
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 4 deletions.
3 changes: 3 additions & 0 deletions locales/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,9 @@ passwordLessLogin: "Password-less login"
passwordLessLoginDescription: "Allows password-less login using a security- or passkey only"
resetPassword: "Reset password"
newPasswordIs: "The new password is \"{password}\""
hideNavFooter: "Automatically hide footer buttons on mobile devices"
reduceUiMargin: "Reduce UI margins on mobile devices"
largeNoteText: "Slightly enlarge note texts"
reduceUiAnimation: "Reduce UI animations"
share: "Share"
notFound: "Not found"
Expand Down
12 changes: 12 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1950,6 +1950,18 @@ export interface Locale extends ILocale {
* 新しいパスワードは「{password}」です
*/
"newPasswordIs": ParameterizedString<"password">;
/**
* モバイルデバイスのとき下のボタンを自動で隠す
*/
"hideNavFooter": string;
/**
* モバイルデバイスのときUIの余白を減らす
*/
"reduceUiMargin": string;
/**
* ノートの文字を少し大きくする
*/
"largeNoteText": string;
/**
* UIのアニメーションを減らす
*/
Expand Down
3 changes: 3 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,9 @@ passwordLessLogin: "パスワードレスログイン"
passwordLessLoginDescription: "パスワードを使用せず、セキュリティキーやパスキーなどのみでログインします"
resetPassword: "パスワードをリセット"
newPasswordIs: "新しいパスワードは「{password}」です"
hideNavFooter: "モバイルデバイスのとき下のボタンを自動で隠す"
reduceUiMargin: "モバイルデバイスのときUIの余白を減らす"
largeNoteText: "ノートの文字を少し大きくする"
reduceUiAnimation: "UIのアニメーションを減らす"
share: "共有"
notFound: "見つかりません"
Expand Down
21 changes: 19 additions & 2 deletions packages/frontend/src/components/MkNote.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar :class="$style.collapsedRenoteTargetAvatar" :user="appearNote.user" link preview/>
<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">
<article v-else :class="[$style.article, defaultStore.state.reduceMargin ? $style.reduceMargin : null]" @contextmenu.stop="onContextmenu">
<div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div>
<MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/>
<div :class="$style.main">
Expand All @@ -64,7 +64,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCwButton v-model="showContent" :text="appearNote.text" :renote="appearNote.renote" :files="appearNote.files" :poll="appearNote.poll" style="margin: 4px 0;"/>
</p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
<div :class="[$style.text, defaultStore.state.largeNoteText ? $style.largeText : null]">
<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="ti ti-arrow-back-up"></i></MkA>
<Mfm
Expand Down Expand Up @@ -878,6 +878,11 @@ function emitUpdReaction(emoji: string, delta: number) {
overflow-wrap: break-word;
}

.text.largeText {
font-size: 1.1em;
line-height: 1.4;
}

.replyIcon {
color: var(--MI_THEME-accent);
margin-right: 0.5em;
Expand Down Expand Up @@ -951,6 +956,10 @@ function emitUpdReaction(emoji: string, delta: number) {
padding: 24px 26px;
}

.article.reduceMargin {
padding: 10px 14px 17px;
}

.avatar {
width: 50px;
height: 50px;
Expand All @@ -970,6 +979,10 @@ function emitUpdReaction(emoji: string, delta: number) {
padding: 20px 22px;
}

.article.reduceMargin {
padding: 9px 13px 16px;
}

.footer {
margin-bottom: -8px;
}
Expand All @@ -992,6 +1005,10 @@ function emitUpdReaction(emoji: string, delta: number) {
.article {
padding: 14px 16px;
}

.article.reduceMargin {
padding: 8px 11px 13px;
}
}

@container (max-width: 450px) {
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/pages/settings/general.vue
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ SPDX-License-Identifier: AGPL-3.0-only

<div class="_gaps_m">
<div class="_gaps_s">
<MkSwitch v-model="reduceMargin">{{ i18n.ts.reduceUiMargin }}</MkSwitch>
<MkSwitch v-model="largeNoteText">{{ i18n.ts.largeNoteText }}</MkSwitch>
<MkSwitch v-model="hideNavFooter">{{ i18n.ts.hideNavFooter }}</MkSwitch>
<MkSwitch v-model="reduceAnimation">{{ i18n.ts.reduceUiAnimation }}</MkSwitch>
<MkSwitch v-model="useBlurEffect">{{ i18n.ts.useBlurEffect }}</MkSwitch>
<MkSwitch v-model="useBlurEffectForModal">{{ i18n.ts.useBlurEffectForModal }}</MkSwitch>
Expand Down Expand Up @@ -284,6 +287,9 @@ const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showC
const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
const reduceMargin = computed(defaultStore.makeGetterSetter('reduceMargin'));
const largeNoteText = computed(defaultStore.makeGetterSetter('largeNoteText'));
const hideNavFooter = computed(defaultStore.makeGetterSetter('hideNavFooter'));
const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
Expand Down Expand Up @@ -364,6 +370,9 @@ watch([
alwaysConfirmFollow,
confirmWhenRevealingSensitiveMedia,
contextMenu,
reduceMargin,
largeNoteText,
hideNavFooter,
], async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
Expand Down
16 changes: 15 additions & 1 deletion packages/frontend/src/pages/timeline.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkStickyContainer>
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800">
<MkSpacer :contentMax="800" :marginMin="marginMin">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl">
<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
Expand Down Expand Up @@ -63,6 +63,16 @@ provide('shouldOmitHeaderTitle', true);
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>();

const DESKTOP_THRESHOLD = 1100;
const MOBILE_THRESHOLD = 500;

const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
const isMobile = ref(deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD);

window.addEventListener('resize', () => {
isMobile.value = deviceKind === 'smartphone' || window.innerWidth <= MOBILE_THRESHOLD;
});

const router = useRouter();

type TimelinePageSrc = BasicTimelineType | `list:${string}`;
Expand Down Expand Up @@ -351,6 +361,10 @@ const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(
iconOnly: true,
}))] as Tab[]);

const marginMin = computed(() =>
(defaultStore.state.reduceMargin && isMobile.value) ? 0 : 16
);

definePageMetadata(() => ({
title: i18n.ts.timeline,
icon: isBasicTimeline(src.value) ? basicTimelineIconClass(src.value) : 'ti ti-home',
Expand Down
12 changes: 12 additions & 0 deletions packages/frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,18 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
hideNavFooter: {
where: 'device',
default: false,
},
reduceMargin: {
where: 'device',
default: true,
},
largeNoteText: {
where: 'device',
default: true,
},
animation: {
where: 'device',
default: !window.matchMedia('(prefers-reduced-motion)').matches,
Expand Down
47 changes: 46 additions & 1 deletion packages/frontend/src/ui/universal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,13 @@ SPDX-License-Identifier: AGPL-3.0-only

<button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>

<div v-if="isMobile" ref="navFooter" :class="$style.nav">
<Transition
:enterActiveClass="$style.transition_navFooter_enterActive"
:leaveActiveClass="$style.transition_navFooter_leaveActive"
:enterFromClass="$style.transition_navFooter_enterFrom"
:leaveToClass="$style.transition_navFooter_leaveTo"
>
<div v-if="isMobile && navFooterShowing" ref="navFooter" :class="$style.nav">
<button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
Expand All @@ -38,6 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button :class="$style.navButton" class="_button" @click="widgetsShowing = true"><i :class="$style.navButtonIcon" class="ti ti-apps"></i></button>
<button :class="$style.postButton" class="_button" @click="os.post()"><i :class="$style.navButtonIcon" class="ti ti-pencil"></i></button>
</div>
</Transition>

<Transition
:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
Expand Down Expand Up @@ -134,6 +141,7 @@ window.addEventListener('resize', () => {

const pageMetadata = ref<null | PageMetadata>(null);
const widgetsShowing = ref(false);
const navFooterShowing = ref(true);
const navFooter = shallowRef<HTMLElement>();
const contents = shallowRef<InstanceType<typeof MkStickyContainer>>();

Expand Down Expand Up @@ -194,12 +202,35 @@ defaultStore.loaded.then(() => {
}
});

let scrollHistory = [];
onMounted(() => {
if (!isDesktop.value) {
window.addEventListener('resize', () => {
if (window.innerWidth >= DESKTOP_THRESHOLD) isDesktop.value = true;
}, { passive: true });
}
if (defaultStore.state.hideNavFooter) {
contents.value.rootEl.addEventListener('scroll', () => {
const now = new Date();
scrollHistory = scrollHistory.filter(x => (now - x.time < 2000) && (now > x.time));
let scrollPosition = contents.value.rootEl.scrollTop;
scrollHistory.push({ time: now, position: scrollPosition });
if (scrollHistory.length === 1) {
return;
}
let diffPosition = scrollPosition - scrollHistory[0].position;
let diffTime = now - scrollHistory[0].time;
let scrollSpeed = diffPosition / diffTime;
if (scrollPosition === 0) {
navFooterShowing.value = true;
scrollHistory = [];
} else if (scrollSpeed > 0.2 && diffPosition > 300 || scrollSpeed < -0.5 && diffPosition < -600) {
navFooterShowing.value = false;
} else if (-0.2 < scrollSpeed && scrollSpeed < 0.02) {
navFooterShowing.value = true;
}
}, { passive: true });
}
});

const iconOnly = ref(false);
Expand Down Expand Up @@ -284,6 +315,20 @@ body {
$ui-font-size: 1em; // TODO: どこかに集約したい
$widgets-hide-threshold: 1090px;

.transition_navFooter_enterActive {
opacity: 1;
transition: opacity 300ms cubic-bezier(0.23, 1, 0.32, 1);
}
.transition_navFooter_leaveActive {
opacity: 1;
transition: opacity 800ms cubic-bezier(0.23, 1, 0.32, 1);
}

.transition_navFooter_enterFrom,
.transition_navFooter_leaveTo {
opacity: 0;
}

.transition_menuDrawerBg_enterActive,
.transition_menuDrawerBg_leaveActive {
opacity: 1;
Expand Down

0 comments on commit 06e2d70

Please sign in to comment.