Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): スワイプやボタンでタイムラインを再読込する機能 #12113

Merged
merged 29 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
cb4cf50
pc reloading
slofp Oct 21, 2023
e9d0e5b
add: disable TL websocket option
slofp Oct 22, 2023
97d5434
fix: stream disconnect when reload
slofp Oct 22, 2023
9e63971
add: pull to refresh
slofp Oct 22, 2023
3d9ca5d
Merge remote-tracking branch 'upstream/develop' into pr-push-refresh
slofp Oct 22, 2023
3558b33
fix: pull to refresh
slofp Oct 22, 2023
47e994b
add changelog
slofp Oct 22, 2023
aab714b
fact: change to disableStreamingTimeline
slofp Oct 22, 2023
8435a4b
lint
slofp Oct 22, 2023
7354c55
remove: en-US text
slofp Oct 22, 2023
546a994
Merge branch 'develop' into pr/12113
syuilo Oct 23, 2023
b078944
Merge branch 'develop' into pr/12113
syuilo Oct 23, 2023
c79b685
Merge branch 'develop' into pr/12113
syuilo Oct 23, 2023
60b8fc7
Merge branch 'develop' into pr/12113
syuilo Oct 23, 2023
3f4234d
refactor
syuilo Oct 23, 2023
e3c3702
refactor
syuilo Oct 23, 2023
3a5e183
add license identifier
syuilo Oct 23, 2023
28efb71
tweak
syuilo Oct 23, 2023
1105741
Update MkPullToRefresh.vue
syuilo Oct 23, 2023
9705a7c
Update MkPullToRefresh.vue
syuilo Oct 23, 2023
c6ce446
Merge remote-tracking branch 'upstream/develop' into pr-push-refresh-1
slofp Oct 27, 2023
cfe3671
Merge branch 'develop' into pr-push-refresh
syuilo Oct 29, 2023
161608f
Merge branch 'develop' into pr/12113
syuilo Oct 29, 2023
06a630a
Merge branch 'pr-push-refresh' of https://github.com/slofp/misskey in…
syuilo Oct 29, 2023
45e1604
change name timeoutHeartBeat
slofp Oct 29, 2023
47ca54c
Merge branch 'develop' into pr/12113
syuilo Oct 29, 2023
552a74a
Merge branch 'pr-push-refresh' of https://github.com/slofp/misskey in…
syuilo Oct 29, 2023
cf16aec
tweak
syuilo Oct 30, 2023
943e3a4
:art:
syuilo Oct 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
https://misskey-hub.net/docs/advanced/publish-on-your-website.html
- Enhance: スワイプしてタイムラインを再読込できるように
- PCの場合は右上のボタンからでも再読込できます
- Enhance: タイムラインの自動更新を無効にできるように
- Enhance: コードのシンタックスハイライトエンジンをShikiに変更
- AiScriptのシンタックスハイライトに対応
- MFMでAiScriptをハイライトする場合、コードブロックの開始部分を ` ```is ` もしくは ` ```aiscript ` としてください
Expand Down
4 changes: 4 additions & 0 deletions locales/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,10 @@ export interface Locale {
"angle": string;
"flip": string;
"showAvatarDecorations": string;
"releaseToRefresh": string;
"refreshing": string;
"pullDownToRefresh": string;
"disableStreamingTimeline": string;
"_announcement": {
"forExistingUsers": string;
"forExistingUsersDescription": string;
Expand Down
4 changes: 4 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1149,6 +1149,10 @@ detach: "外す"
angle: "角度"
flip: "反転"
showAvatarDecorations: "アイコンのデコレーションを表示"
releaseToRefresh: "離してリロード"
refreshing: "リロード中"
pullDownToRefresh: "引っ張ってリロード"
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"

_announcement:
forExistingUsers: "既存ユーザーのみ"
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/src/boot/main-boot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { common } from './common.js';
import { version, ui, lang, updateLocale } from '@/config.js';
import { i18n, updateI18n } from '@/i18n.js';
import { confirm, alert, post, popup, toast } from '@/os.js';
import { useStream } from '@/stream.js';
import { useStream, isReloading } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js';
Expand Down Expand Up @@ -39,6 +39,7 @@ export async function mainBoot() {

let reloadDialogShowing = false;
stream.on('_disconnected_', async () => {
if (isReloading) return;
if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
location.reload();
} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
Expand Down
2 changes: 2 additions & 0 deletions packages/frontend/src/components/MkPageWindow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ defineExpose({

<style lang="scss" module>
.root {
overscroll-behavior: none;

min-height: 100%;
background: var(--bg);

Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/src/components/MkPagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ const props = withDefaults(defineProps<{

const emit = defineEmits<{
(ev: 'queue', count: number): void;
(ev: 'status', error: boolean): void;
}>();

let rootEl = $shallowRef<HTMLElement>();
Expand Down Expand Up @@ -193,6 +194,11 @@ watch(queue, (a, b) => {
emit('queue', queue.value.size);
}, { deep: true });

watch(error, (n, o) => {
if (n === o) return;
emit('status', n);
});

async function init(): Promise<void> {
items.value = new Map();
queue.value = new Map();
Expand Down
238 changes: 238 additions & 0 deletions packages/frontend/src/components/MkPullToRefresh.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
<div ref="rootEl">
<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${currentHeight / 3}px;`">
<div :class="$style.frameContent">
<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
<div :class="$style.text">
<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
</div>
</div>
</div>
<div :class="{ [$style.slotClip]: isPullStart }">
<slot/>
</div>
</div>
</template>

<script lang="ts" setup>
import { onMounted, onUnmounted } from 'vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js';

const SCROLL_STOP = 10;
const MAX_PULL_DISTANCE = Infinity;
const FIRE_THRESHOLD = 200;
const RELEASE_TRANSITION_DURATION = 200;

let isPullStart = $ref(false);
let isPullEnd = $ref(false);
let isRefreshing = $ref(false);
let currentHeight = $ref(0);

let supportPointerDesktop = false;
let startScreenY: number | null = null;

const rootEl = $shallowRef<HTMLDivElement>();
let scrollEl: HTMLElement | null = null;

let disabled = false;

const emits = defineEmits<{
(ev: 'refresh'): void;
}>();

function getScrollableParentElement(node) {
if (node == null) {
return null;
}

if (node.scrollHeight > node.clientHeight) {
return node;
} else {
return getScrollableParentElement(node.parentNode);
}
}

function getScreenY(event) {
if (supportPointerDesktop) {
return event.screenY;
}
return event.touches[0].screenY;
}

function moveStart(event) {
if (!isPullStart && !isRefreshing && !disabled) {
isPullStart = true;
startScreenY = getScreenY(event);
currentHeight = 0;
}
}

function moveBySystem(to: number): Promise<void> {
return new Promise(r => {
const startHeight = currentHeight;
const overHeight = currentHeight - to;
if (overHeight < 1) {
r();
return;
}
const startTime = Date.now();
let intervalId = setInterval(() => {
const time = Date.now() - startTime;
if (time > RELEASE_TRANSITION_DURATION) {
currentHeight = to;
clearInterval(intervalId);
r();
return;
}
const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
if (currentHeight < nextHeight) return;
currentHeight = nextHeight;
}, 1);
});
}

async function fixOverContent() {
if (currentHeight > FIRE_THRESHOLD) {
await moveBySystem(FIRE_THRESHOLD);
}
}

async function closeContent() {
if (currentHeight > 0) {
await moveBySystem(0);
}
}

function moveEnd() {
if (isPullStart && !isRefreshing) {
startScreenY = null;
if (isPullEnd) {
isPullEnd = false;
isRefreshing = true;
fixOverContent().then(() => emits('refresh'));
} else {
closeContent().then(() => isPullStart = false);
}
}
}

function moving(event) {
if (!isPullStart || isRefreshing || disabled) return;

if (!scrollEl) {
scrollEl = getScrollableParentElement(rootEl);
}
if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + currentHeight)) {
currentHeight = 0;
isPullEnd = false;
moveEnd();
return;
}

if (startScreenY === null) {
startScreenY = getScreenY(event);
}
const moveScreenY = getScreenY(event);

const moveHeight = moveScreenY - startScreenY!;
currentHeight = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);

isPullEnd = currentHeight >= FIRE_THRESHOLD;
}

/**
* emit(refresh)が完了したことを知らせる関数
*
* タイムアウトがないのでこれを最終的に実行しないと出たままになる
*/
function refreshFinished() {
closeContent().then(() => {
isPullStart = false;
isRefreshing = false;
});
}

function setDisabled(value) {
disabled = value;
}

onMounted(() => {
supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';

if (supportPointerDesktop) {
rootEl.addEventListener('pointerdown', moveStart);
// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
window.addEventListener('pointerup', moveEnd);
rootEl.addEventListener('pointermove', moving, { passive: true });
} else {
rootEl.addEventListener('touchstart', moveStart);
rootEl.addEventListener('touchend', moveEnd);
rootEl.addEventListener('touchmove', moving, { passive: true });
}
});

onUnmounted(() => {
if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
});

defineExpose({
refreshFinished,
setDisabled,
});
</script>

<style lang="scss" module>
.frame {
position: relative;
overflow: clip;

width: 100%;
min-height: var(--frame-min-height, 0px);

box-shadow: inset 0px -7px 10px -10px rgba(0,0,0,.1);
mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent);
-webkit-mask-image: -webkit-linear-gradient(90deg, #000 0%, #000 80%, transparent);

pointer-events: none;
}

.frameContent {
position: absolute;
bottom: 0;
width: 100%;
margin: 5px 0;
display: flex;
flex-direction: column;
align-items: center;
font-size: 14px;

> .icon, > .loader {
margin: 6px 0;
}

> .icon {
transition: transform .25s;

&.refresh {
transform: rotate(180deg);
}
}

> .text {
margin: 5px 0;
}
}

.slotClip {
overflow-y: clip;
}
</style>
Loading
Loading