Merge tag '2023.10.0' into merge-upstream

This commit is contained in:
riku6460
2023-10-10 21:22:31 +09:00
226 changed files with 5462 additions and 2914 deletions

View File

@@ -4,10 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<button class="_button" :class="$style.root" @mousedown="toggle">
<b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b>
<span v-if="!modelValue" :class="$style.label">{{ label }}</span>
</button>
<MkButton rounded full small @click="toggle"><b>{{ modelValue ? i18n.ts._cw.hide : i18n.ts._cw.show }}</b><span v-if="!modelValue" :class="$style.label">{{ label }}</span></MkButton>
</template>
<script lang="ts" setup>
@@ -15,6 +12,7 @@ import { computed } from 'vue';
import * as Misskey from 'misskey-js';
import { concat } from '@/scripts/array.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
const props = defineProps<{
modelValue: boolean;
@@ -33,25 +31,12 @@ const label = computed(() => {
] as string[][]).join(' / ');
});
const toggle = () => {
function toggle() {
emit('update:modelValue', !props.modelValue);
};
}
</script>
<style lang="scss" module>
.root {
display: inline-block;
padding: 4px 8px;
font-size: 0.7em;
color: var(--cwFg);
background: var(--cwBg);
border-radius: 2px;
&:hover {
background: var(--cwHoverBg);
}
}
.label {
margin-left: 4px;

View File

@@ -45,8 +45,11 @@ import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { useRouter } from '@/router.js';
import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
const router = useRouter();
const props = withDefaults(defineProps<{
file: Misskey.entities.DriveFile;
folder: Misskey.entities.DriveFolder | null;
@@ -71,7 +74,7 @@ function onClick(ev: MouseEvent) {
if (props.selectMode) {
emit('chosen', props.file);
} else {
os.popupMenu(getDriveFileMenu(props.file, props.folder), (ev.currentTarget ?? ev.target ?? undefined) as HTMLElement | undefined);
router.push(`/my/drive/file/${props.file.id}`);
}
}

View File

@@ -23,6 +23,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:spellcheck="spellcheck"
:step="step"
:list="id"
:min="min"
:max="max"
@focus="onFocus"
@blur="focused = false"
@keydown="onKeydown($event)"
@@ -59,6 +61,8 @@ const props = defineProps<{
spellcheck?: boolean;
step?: any;
datalist?: string[];
min?: number;
max?: number;
inline?: boolean;
debounce?: boolean;
manualSave?: boolean;

View File

@@ -17,7 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
:title="media.name"
controls
preload="metadata"
@volumechange="volumechange"
/>
</div>
<a
@@ -33,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { onMounted, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { soundConfigStore } from '@/scripts/sound.js';
import { i18n } from '@/i18n.js';
@@ -43,15 +42,13 @@ const props = withDefaults(defineProps<{
}>(), {
});
const audioEl = $shallowRef<HTMLAudioElement | null>();
const audioEl = shallowRef<HTMLAudioElement>();
let hide = $ref(true);
function volumechange() {
if (audioEl) soundConfigStore.set('mediaVolume', audioEl.volume);
}
onMounted(() => {
if (audioEl) audioEl.volume = soundConfigStore.state.mediaVolume;
watch(audioEl, () => {
if (audioEl.value) {
audioEl.value.volume = 0.3;
}
});
</script>

View File

@@ -14,6 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else :class="[$style.visible, (video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitiveContainer]">
<video
ref="videoEl"
:class="$style.video"
:poster="video.thumbnailUrl"
:title="video.comment"
@@ -31,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { ref, shallowRef, watch } from 'vue';
import * as Misskey from 'misskey-js';
import bytes from '@/filters/bytes.js';
import { defaultStore } from '@/store.js';
@@ -42,6 +43,14 @@ const props = defineProps<{
}>();
const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.enableDataSaverMode) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
const videoEl = shallowRef<HTMLVideoElement>();
watch(videoEl, () => {
if (videoEl.value) {
videoEl.value.volume = 0.3;
}
});
</script>
<style lang="scss" module>

View File

@@ -54,7 +54,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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" :i="$i"/>
<MkCwButton v-model="showContent" :note="appearNote"/>
<MkCwButton v-model="showContent" :note="appearNote" style="margin: 4px 0;"/>
</p>
<div v-show="appearNote.cw == null || showContent" :class="[{ [$style.contentCollapsed]: collapsed }]">
<div :class="$style.text">
@@ -165,7 +165,7 @@ import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
import { MenuItem } from '@/types/menu';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js';
@@ -211,11 +211,11 @@ const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : n
const isLong = shouldCollapsed(appearNote);
const collapsed = ref(appearNote.cw == null && isLong);
const isDeleted = ref(false);
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
const translation = ref<any>(null);
const translating = ref(false);
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || appearNote.userId === $i.id);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
const keymap = {

View File

@@ -93,9 +93,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<footer>
<div :class="$style.noteFooterInfo">
<div v-if="appearNote.updatedAt">
{{ i18n.ts.edited }}: <MkTime :time="appearNote.updatedAt" mode="detail"/>
</div>
<MkA :to="notePage(appearNote)">
<MkTime :time="appearNote.createdAt" mode="detail"/>
</MkA>
@@ -214,7 +211,7 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { MenuItem } from '@/types/menu';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -258,7 +255,7 @@ let appearNote = $computed(() => isRenote ? note.renote as Misskey.entities.Note
const isMyRenote = $i && ($i.id === note.userId);
const showContent = ref(false);
const isDeleted = ref(false);
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
const translation = ref(null);
const translating = ref(false);
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;

View File

@@ -14,7 +14,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/>
</div>
<div :class="$style.info">
<span v-if="note.updatedAt" style="margin-right: 0.5em;" :title="i18n.ts.edited"><i class="ti ti-pencil"></i></span>
<MkA :to="notePage(note)">
<MkTime :time="note.createdAt"/>
</MkA>

View File

@@ -49,9 +49,9 @@ 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";
import { checkWordMute } from "@/scripts/check-word-mute";
import { defaultStore } from "@/store";
import { userPage } from '@/filters/user.js';
import { checkWordMute } from '@/scripts/check-word-mute.js';
import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
note: Misskey.entities.Note;
@@ -63,7 +63,7 @@ const props = withDefaults(defineProps<{
depth: 1,
});
const muted = ref(checkWordMute(props.note, $i, defaultStore.state.mutedWords));
const muted = ref($i ? checkWordMute(props.note, $i, $i.mutedWords) : false);
let showContent = $ref(false);
let replies: Misskey.entities.Note[] = $ref([]);

View File

@@ -143,7 +143,6 @@ const props = withDefaults(defineProps<{
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
updateMode?: boolean;
}>(), {
initialVisibleUsers: () => [],
autofocus: true,
@@ -710,7 +709,6 @@ async function post(ev?: MouseEvent) {
visibility: visibility,
visibleUserIds: visibility === 'specified' ? visibleUsers.map(u => u.id) : undefined,
reactionAcceptance,
noteId: props.updateMode ? props.initialNote?.id : undefined,
};
if (withHashtags && hashtags && hashtags.trim() !== '') {
@@ -733,7 +731,7 @@ async function post(ev?: MouseEvent) {
}
posting = true;
os.api(props.updateMode ? 'notes/update' : 'notes/create', postData, token).then(() => {
os.api('notes/create', postData, token).then(() => {
if (props.freezeAfterPosted) {
posted = true;
} else {

View File

@@ -30,7 +30,6 @@ const props = defineProps<{
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;
updateMode?: boolean;
}>();
const emit = defineEmits<{

View File

@@ -30,13 +30,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder>
<MkFolder v-if="availableTos" :defaultOpen="true">
<template #label>{{ i18n.ts.termsOfService }}</template>
<template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template>
<MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true">
<template #label>{{ tosPrivacyPolicyLabel }}</template>
<template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
<div class="_gaps_s">
<div v-if="availableTos"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
</div>
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a>
<MkSwitch :modelValue="agreeTos" style="margin-top: 16px;" @update:modelValue="updateAgreeTos">{{ i18n.ts.agree }}</MkSwitch>
<MkSwitch :modelValue="agreeTosAndPrivacyPolicy" style="margin-top: 16px;" @update:modelValue="updateAgreeTosAndPrivacyPolicy">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder>
<MkFolder :defaultOpen="true">
@@ -70,14 +72,15 @@ import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
const availableServerRules = instance.serverRules.length > 0;
const availableTos = instance.tosUrl != null;
const availableTos = instance.tosUrl != null && instance.tosUrl !== '';
const availablePrivacyPolicy = instance.privacyPolicyUrl != null && instance.privacyPolicyUrl !== '';
const agreeServerRules = ref(false);
const agreeTos = ref(false);
const agreeTosAndPrivacyPolicy = ref(false);
const agreeNote = ref(false);
const agreed = computed(() => {
return (!availableServerRules || agreeServerRules.value) && (!availableTos || agreeTos.value) && agreeNote.value;
return (!availableServerRules || agreeServerRules.value) && ((!availableTos && !availablePrivacyPolicy) || agreeTosAndPrivacyPolicy.value) && agreeNote.value;
});
const emit = defineEmits<{
@@ -85,6 +88,18 @@ const emit = defineEmits<{
(ev: 'done'): void;
}>();
const tosPrivacyPolicyLabel = computed(() => {
if (availableTos && availablePrivacyPolicy) {
return i18n.ts.tosAndPrivacyPolicy;
} else if (availableTos) {
return i18n.ts.termsOfService;
} else if (availablePrivacyPolicy) {
return i18n.ts.privacyPolicy;
} else {
return "";
}
});
async function updateAgreeServerRules(v: boolean) {
if (v) {
const confirm = await os.confirm({
@@ -99,17 +114,19 @@ async function updateAgreeServerRules(v: boolean) {
}
}
async function updateAgreeTos(v: boolean) {
async function updateAgreeTosAndPrivacyPolicy(v: boolean) {
if (v) {
const confirm = await os.confirm({
type: 'question',
title: i18n.ts.doYouAgree,
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }),
text: i18n.t('iHaveReadXCarefullyAndAgree', {
x: tosPrivacyPolicyLabel.value,
}),
});
if (confirm.canceled) return;
agreeTos.value = true;
agreeTosAndPrivacyPolicy.value = true;
} else {
agreeTos.value = false;
agreeTosAndPrivacyPolicy.value = false;
}
}

View File

@@ -13,6 +13,7 @@ import MkNotes from '@/components/MkNotes.vue';
import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js';
import { $i } from '@/account.js';
import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
@@ -23,11 +24,9 @@ const props = withDefaults(defineProps<{
role?: string;
sound?: boolean;
withRenotes?: boolean;
withReplies?: boolean;
onlyFiles?: boolean;
}>(), {
withRenotes: true,
withReplies: false,
onlyFiles: false,
});
@@ -40,7 +39,15 @@ provide('inChannel', computed(() => props.src === 'channel'));
const tlComponent: InstanceType<typeof MkNotes> = $ref();
let tlNotesCount = 0;
const prepend = note => {
tlNotesCount++;
if (instance.notesPerOneAd > 0 && tlNotesCount % instance.notesPerOneAd === 0) {
note._shouldInsertAd_ = true;
}
tlComponent.pagingComponent?.prepend(note);
emit('note');
@@ -70,12 +77,10 @@ if (props.src === 'antenna') {
endpoint = 'notes/timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
};
connection = stream.useChannel('homeTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
});
connection.on('note', prepend);
@@ -85,12 +90,10 @@ if (props.src === 'antenna') {
endpoint = 'notes/local-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
};
connection = stream.useChannel('localTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
});
connection.on('note', prepend);
@@ -109,12 +112,10 @@ if (props.src === 'antenna') {
endpoint = 'notes/hybrid-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
};
connection = stream.useChannel('hybridTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
});
connection.on('note', prepend);
@@ -122,12 +123,10 @@ if (props.src === 'antenna') {
endpoint = 'notes/global-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
};
connection = stream.useChannel('globalTimeline', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
});
connection.on('note', prepend);
@@ -150,14 +149,10 @@ if (props.src === 'antenna') {
} else if (props.src === 'list') {
endpoint = 'notes/user-list-timeline';
query = {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
};
connection = stream.useChannel('userList', {
withRenotes: props.withRenotes,
withReplies: props.withReplies,
withFiles: props.onlyFiles ? true : undefined,
listId: props.list,
});

View File

@@ -104,7 +104,25 @@ function showMenu(ev) {
action: () => {
os.pageWindow('/about-misskey');
},
}, null, {
}, null, (instance.impressumUrl) ? {
text: i18n.ts.impressum,
icon: 'ti ti-file-invoice',
action: () => {
window.open(instance.impressumUrl, '_blank');
},
} : undefined, (instance.tosUrl) ? {
text: i18n.ts.termsOfService,
icon: 'ti ti-notebook',
action: () => {
window.open(instance.tosUrl, '_blank');
},
} : undefined, (instance.privacyPolicyUrl) ? {
text: i18n.ts.privacyPolicy,
icon: 'ti ti-shield-lock',
action: () => {
window.open(instance.privacyPolicyUrl, '_blank');
},
} : undefined, (!instance.impressumUrl && !instance.tosUrl && !instance.privacyPolicyUrl) ? undefined : null, {
text: i18n.ts.help,
icon: 'ti ti-help-circle',
action: () => {