merge upstream

This commit is contained in:
Hazelnoot 2025-03-25 16:14:53 -04:00
commit d8908ef2d8
1065 changed files with 32953 additions and 20092 deletions

View file

@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.headerRight">
<template v-if="!(channel != null && fixed)">
<button v-if="channel == null" ref="visibilityButton" v-click-anime v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" :disabled="editId != null" @click="setVisibility">
<button v-if="channel == null" ref="visibilityButton" v-tooltip="i18n.ts.visibility" :class="['_button', $style.headerRightItem, $style.visibility]" :disabled="editId != null" @click="setVisibility">
<span v-if="visibility === 'public'"><i class="ti ti-world"></i></span>
<span v-if="visibility === 'home'"><i class="ti ti-home"></i></span>
<span v-if="visibility === 'followers'"><i class="ti ti-lock"></i></span>
@ -32,11 +32,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<span :class="$style.headerRightButtonText">{{ channel.name }}</span>
</button>
</template>
<button v-click-anime v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified' || editId != null" @click="toggleLocalOnly">
<button v-tooltip="i18n.ts._visibility.disableFederation" class="_button" :class="[$style.headerRightItem, { [$style.danger]: localOnly }]" :disabled="channel != null || visibility === 'specified' || editId != null" @click="toggleLocalOnly">
<span v-if="!localOnly"><i class="ti ti-rocket"></i></span>
<span v-else><i class="ti ti-rocket-off"></i></span>
</button>
<button v-click-anime v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<button ref="otherSettingsButton" v-tooltip="i18n.ts.other" class="_button" :class="$style.headerRightItem" @click="showOtherSettings"><i class="ti ti-dots"></i></button>
<button v-tooltip="i18n.ts.reactionAcceptance" class="_button" :class="[$style.headerRightItem, { [$style.danger]: reactionAcceptance === 'likeOnly' }]" @click="toggleReactionAcceptance">
<span v-if="reactionAcceptance === 'likeOnly'"><i class="ti ti-heart"></i></span>
<span v-else-if="reactionAcceptance === 'likeOnlyForRemote'"><i class="ti ti-heart-plus"></i></span>
<span v-else><i class="ph-smiley ph-bold ph-lg"></i></span>
@ -65,9 +66,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<MkInfo v-if="hasNotSpecifiedMentions" warn :class="$style.hasNotSpecifiedMentions">{{ i18n.ts.notSpecifiedMentionWarning }} - <button class="_textButton" @click="addMissingMention()">{{ i18n.ts.add }}</button></MkInfo>
<div v-show="useCw" :class="$style.cwFrame">
<div v-show="useCw" :class="$style.cwOuter">
<input ref="cwInputEl" v-model="cw" :class="$style.cw" :placeholder="i18n.ts.annotation" @keydown="onKeydown" @keyup="onKeyup" @compositionend="onCompositionEnd">
<div v-if="maxCwLength - cwLength < 100" :class="['_acrylic', $style.textCount, { [$style.textOver]: cwLength > maxCwLength }]">{{ maxCwLength - cwLength }}</div>
<div v-if="maxCwTextLength - cwTextLength < 20" :class="['_acrylic', $style.cwTextCount, { [$style.cwTextOver]: cwTextLength > maxCwTextLength }]">{{ maxCwTextLength - cwTextLength }}</div>
</div>
<div :class="[$style.textOuter, { [$style.withCw]: useCw }]">
<div v-if="channel" :class="$style.colorBar" :style="{ background: channel.color }"></div>
@ -106,41 +107,48 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, toRaw, type ShallowRef } from 'vue';
import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide, shallowRef, ref, computed, useTemplateRef, toRaw } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import insertTextAtCursor from 'insert-text-at-cursor';
import { toASCII } from 'punycode.js';
import { host, url } from '@@/js/config.js';
import type { ShallowRef } from 'vue';
import { appendContentWarning } from '@@/js/append-content-warning.js';
import type { MenuItem } from '@/types/menu.js';
import type { PostFormProps } from '@/types/post-form.js';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
import MkNotePreview from '@/components/MkNotePreview.vue';
import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
import MkPollEditor, { type PollEditorModelValue } from '@/components/MkPollEditor.vue';
import { erase, unique } from '@/scripts/array.js';
import { extractMentions } from '@/scripts/extract-mentions.js';
import { formatTimeString } from '@/scripts/format-time-string.js';
import { Autocomplete } from '@/scripts/autocomplete.js';
import XTextCounter from '@/components/MkPostForm.TextCounter.vue';
import MkPollEditor from '@/components/MkPollEditor.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
import { erase, unique } from '@/utility/array.js';
import { extractMentions } from '@/utility/extract-mentions.js';
import { formatTimeString } from '@/utility/format-time-string.js';
import { Autocomplete } from '@/utility/autocomplete.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { selectFiles } from '@/scripts/select-file.js';
import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { selectFiles } from '@/utility/select-file.js';
import { store } from '@/store.js';
import MkInfo from '@/components/MkInfo.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js';
import { uploadFile } from '@/scripts/upload.js';
import { deepClone } from '@/scripts/clone.js';
import { ensureSignin, notesCount, incNotesCount } from '@/i.js';
import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
import { uploadFile } from '@/utility/upload.js';
import { deepClone } from '@/utility/clone.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import MkScheduleEditor from '@/components/MkScheduleEditor.vue';
import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/utility/achievements.js';
import { emojiPicker } from '@/utility/emoji-picker.js';
import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
import { prefer } from '@/preferences.js';
import { getPluginHandlers } from '@/plugin.js';
import { DI } from '@/di.js';
const $i = signinRequired();
const $i = ensureSignin();
const modal = inject('modal');
@ -157,7 +165,7 @@ const props = withDefaults(defineProps<PostFormProps & {
initialLocalOnly: undefined,
});
provide('mock', props.mock);
provide(DI.mock, props.mock);
const emit = defineEmits<{
(ev: 'posted'): void;
@ -168,10 +176,11 @@ const emit = defineEmits<{
(ev: 'fileChangeSensitive', fileId: string, to: boolean): void;
}>();
const textareaEl = shallowRef<HTMLTextAreaElement | null>(null);
const cwInputEl = shallowRef<HTMLInputElement | null>(null);
const hashtagsInputEl = shallowRef<HTMLInputElement | null>(null);
const visibilityButton = shallowRef<HTMLElement>();
const textareaEl = useTemplateRef('textareaEl');
const cwInputEl = useTemplateRef('cwInputEl');
const hashtagsInputEl = useTemplateRef('hashtagsInputEl');
const visibilityButton = useTemplateRef('visibilityButton');
const otherSettingsButton = useTemplateRef('otherSettingsButton');
const posting = ref(false);
const posted = ref(false);
@ -179,19 +188,18 @@ const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []);
const poll = ref<PollEditorModelValue | null>(null);
const useCw = ref<boolean>(!!props.initialCw);
const showPreview = ref(defaultStore.state.showPreview);
watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction);
watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
const showPreview = ref(store.s.showPreview);
watch(showPreview, () => store.set('showPreview', showPreview.value));
const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value));
const cw = ref<string | null>(props.initialCw ?? null);
const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly));
const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility));
const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly));
const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility));
const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
if (props.initialVisibleUsers) {
props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
}
const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
const autocomplete = ref(null);
const reactionAcceptance = ref(store.s.reactionAcceptance);
const draghover = ref(false);
const quoteId = ref<string | null>(null);
const hasNotSpecifiedMentions = ref(false);
@ -204,6 +212,7 @@ const scheduleNote = ref<{
scheduledAt: number | null;
} | null>(null);
const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
const postFormActions = getPluginHandlers('post_form_action');
const draftKey = computed((): string => {
let key = props.channel ? `channel:${props.channel.id}` : '';
@ -255,8 +264,11 @@ const maxTextLength = computed((): number => {
return instance ? instance.maxNoteTextLength : 1000;
});
const cwLength = computed(() => cw.value?.length ?? 0);
const maxCwLength = computed(() => instance.maxCwLength);
const cwTextLength = computed((): number => {
return cw.value?.length ?? 0;
});
const maxCwTextLength = computed(() => instance.maxCwLength);
const canPost = computed((): boolean => {
return !props.mock && !posting.value && !posted.value &&
@ -268,13 +280,19 @@ const canPost = computed((): boolean => {
quoteId.value != null
) &&
(textLength.value <= maxTextLength.value) &&
(cwLength.value <= maxCwLength.value) &&
(
useCw.value ?
(
cw.value != null && cw.value.trim() !== '' &&
cwTextLength.value <= maxCwTextLength.value
) : true
) &&
(files.value.length <= 16) &&
(!poll.value || poll.value.choices.length >= 2);
});
const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));
const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
watch(text, () => {
checkMissingMention();
@ -362,7 +380,7 @@ if (props.specified) {
}
// keep cw when reply
if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
if (prefer.s.keepCw && props.reply && props.reply.cw) {
useCw.value = true;
cw.value = props.reply.cw;
}
@ -484,7 +502,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities
function upload(file: File, name?: string): void {
if (props.mock) return;
uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
uploadFile(file, prefer.s.uploadFolder, name).then(res => {
files.value.push(res);
});
}
@ -505,8 +523,8 @@ function setVisibility() {
}, {
changeVisibility: v => {
visibility.value = v;
if (defaultStore.state.rememberNoteVisibility) {
defaultStore.set('visibility', visibility.value);
if (prefer.s.rememberNoteVisibility) {
store.set('visibility', visibility.value);
}
},
closed: () => dispose(),
@ -553,8 +571,8 @@ async function toggleLocalOnly() {
}
localOnly.value = !localOnly.value;
if (defaultStore.state.rememberNoteVisibility) {
defaultStore.set('localOnly', localOnly.value);
if (prefer.s.rememberNoteVisibility) {
store.set('localOnly', localOnly.value);
}
}
@ -574,6 +592,47 @@ async function toggleReactionAcceptance() {
reactionAcceptance.value = select.result;
}
//#region popup
function showOtherSettings() {
let reactionAcceptanceIcon = 'ti ti-icons';
if (reactionAcceptance.value === 'likeOnly') {
reactionAcceptanceIcon = 'ti ti-heart _love';
} else if (reactionAcceptance.value === 'likeOnlyForRemote') {
reactionAcceptanceIcon = 'ti ti-heart-plus';
}
const menuItems = [{
type: 'component',
component: XTextCounter,
props: {
textLength: textLength,
},
}, { type: 'divider' }, {
icon: reactionAcceptanceIcon,
text: i18n.ts.reactionAcceptance,
action: () => {
toggleReactionAcceptance();
},
}, { type: 'divider' }, {
icon: 'ti ti-trash',
text: i18n.ts.reset,
danger: true,
action: async () => {
if (props.mock) return;
const { canceled } = await os.confirm({
type: 'question',
text: i18n.ts.resetAreYouSure,
});
if (canceled) return;
clear();
},
}] satisfies MenuItem[];
os.popupMenu(menuItems, otherSettingsButton.value);
}
//#endregion
function pushVisibleUser(user: Misskey.entities.UserDetailed) {
if (!visibleUsers.value.some(u => u.username === user.username && u.host === user.host)) {
visibleUsers.value.push(user);
@ -623,6 +682,8 @@ function onCompositionEnd(ev: CompositionEvent) {
justEndedComposition.value = true;
}
const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]';
async function onPaste(ev: ClipboardEvent) {
if (props.mock) return;
if (!ev.clipboardData) return;
@ -633,7 +694,7 @@ async function onPaste(ev: ClipboardEvent) {
if (!file) continue;
const lio = file.name.lastIndexOf('.');
const ext = lio >= 0 ? file.name.slice(lio) : '';
const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
upload(file, formatted);
}
}
@ -678,7 +739,7 @@ async function onPaste(ev: ClipboardEvent) {
return;
}
const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0');
const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0');
const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
upload(file, `${fileName}.txt`);
});
@ -772,19 +833,19 @@ function deleteDraft() {
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
}
async function post(ev?: MouseEvent) {
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
os.alert({
type: 'error',
text: i18n.ts.cwNotationRequired,
});
return;
}
function isAnnoying(text: string): boolean {
return text.includes('$[x2') ||
text.includes('$[x3') ||
text.includes('$[x4') ||
text.includes('$[scale') ||
text.includes('$[position');
}
async function post(ev?: MouseEvent) {
if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
if (el) {
if (el && prefer.s.animation) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
@ -796,14 +857,10 @@ async function post(ev?: MouseEvent) {
if (props.mock) return;
const annoying =
text.value.includes('$[x2') ||
text.value.includes('$[x3') ||
text.value.includes('$[x4') ||
text.value.includes('$[scale') ||
text.value.includes('$[position');
if (annoying && visibility.value === 'public') {
if (visibility.value === 'public' && (
(useCw.value && cw.value != null && cw.value.trim() !== '' && isAnnoying(cw.value)) || // CW
((!useCw.value || cw.value == null || cw.value.trim() === '') && text.value != null && text.value.trim() !== '' && isAnnoying(text.value)) // CW
)) {
const { canceled, result } = await os.actions({
type: 'warning',
text: i18n.ts.thisPostMayBeAnnoying,
@ -884,6 +941,7 @@ async function post(ev?: MouseEvent) {
}
// plugin
const notePostInterruptors = getPluginHandlers('note_post_interruptor');
if (notePostInterruptors.length > 0) {
for (const interruptor of notePostInterruptors) {
try {
@ -898,7 +956,7 @@ async function post(ev?: MouseEvent) {
if (postAccount.value) {
const storedAccounts = await getAccounts();
token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
token = storedAccounts.find(x => x.user.id === postAccount.value?.id)?.token;
}
posting.value = true;
@ -1182,6 +1240,8 @@ defineExpose({
&.modal {
width: 100%;
max-width: 520px;
overflow-x: clip;
overflow-y: auto;
}
}
@ -1292,7 +1352,7 @@ defineExpose({
border-radius: var(--MI-radius-sm);
&:hover {
background: var(--MI_THEME-X5);
background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
}
&:disabled {
@ -1356,7 +1416,7 @@ defineExpose({
margin-right: 14px;
padding: 8px 0 8px 8px;
border-radius: var(--MI-radius-sm);
background: var(--MI_THEME-X4);
background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1));
}
.hasNotSpecifiedMentions {
@ -1387,7 +1447,12 @@ defineExpose({
}
}
.cwFrame {
.cwOuter {
width: 100%;
position: relative;
}
.cw {
z-index: 1;
padding-bottom: 8px;
border-bottom: solid 0.5px var(--MI_THEME-divider);
@ -1396,6 +1461,23 @@ defineExpose({
position: relative;
}
.cwTextCount {
position: absolute;
top: 0;
right: 2px;
padding: 2px 6px;
font-size: .9em;
color: var(--MI_THEME-warn);
border-radius: 6px;
max-width: 100%;
min-width: 1.6em;
text-align: center;
&.cwTextOver {
color: #ff2a2a;
}
}
.hashtags {
z-index: 1;
padding-top: 8px;
@ -1470,7 +1552,7 @@ defineExpose({
border-radius: var(--MI-radius-sm);
&:hover {
background: var(--MI_THEME-X5);
background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05));
}
&.footerButtonActive {