Feat: emoji picker profile

This commit is contained in:
mattyatea 2024-01-14 13:54:37 +09:00
parent 693efb99a0
commit 6fa319afc6
12 changed files with 502 additions and 345 deletions

View file

@ -180,7 +180,9 @@ id: 'aidx'
#outgoingAddressFamily: ipv4
# Proxy for HTTP/HTTPS
#proxy: http://127.0.0.1:3128
#
proxyBypassHosts:
- api.deepl.com

View file

@ -9,6 +9,8 @@ notifications: "Notifications"
username: "Username"
password: "Password"
forgotPassword: "Forgot password"
setDefaultProfileConfirm: "Do you want to make this profile the default?"
emojiPickerProfile: "Emoji picker profile"
fetchingAsApObject: "Fetching from the Fediverse..."
ok: "OK"
gotIt: "Got it!"

3
locales/index.d.ts vendored
View file

@ -14,6 +14,8 @@ export interface Locale {
"forgotPassword": string;
"fetchingAsApObject": string;
"ok": string;
"setDefaultProfileConfirm": string;
"emojiPickerProfile": string;
"notificationIndicator": string;
"hanntenn": string;
"hanntennInfo": string;
@ -1767,6 +1769,7 @@ export interface Locale {
};
"_options": {
"gtlAvailable": string;
"emojiPickerProfileLimit": string;
"ltlAvailable": string;
"canPublicNote": string;
"canEditNote": string;

View file

@ -11,6 +11,8 @@ password: "パスワード"
forgotPassword: "パスワードを忘れた"
fetchingAsApObject: "連合に照会中"
ok: "OK"
setDefaultProfileConfirm: "このプロファイルをデフォルトにしますか?"
emojiPickerProfile: "絵文字ピッカーのプロファイル"
notificationIndicator: "通知のインジケーターの数字を表示する"
hanntenn: "アイコンとバナーを反転させる"
hanntennInfo: "ダークだったらライトのアイコンに、ライトだったらダークのアイコンに。"
@ -1673,6 +1675,7 @@ _role:
high: "高"
_options:
gtlAvailable: "グローバルタイムラインの閲覧"
emojiPickerProfileLimit: "絵文字ピッカーのプロファイルの上限数(最大5)"
ltlAvailable: "ローカルタイムラインの閲覧"
canPublicNote: "パブリック投稿の許可"
canEditNote: "ノートの編集"

View file

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2023.12.2-PrisMisskey.4",
"version": "2023.12.2-PrisMisskey.5",
"codename": "nasubi",
"repository": {
"type": "git",

View file

@ -59,6 +59,7 @@ export type RolePolicies = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
emojiPickerProfileLimit: number;
};
export const DEFAULT_POLICIES: RolePolicies = {
@ -74,7 +75,6 @@ export const DEFAULT_POLICIES: RolePolicies = {
canManageCustomEmojis: false,
canRequestCustomEmojis: false,
canManageAvatarDecorations: false,
canRequestCustomEmojis: false,
canSearchNotes: false,
canUseTranslator: true,
canHideAds: false,
@ -90,6 +90,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
emojiPickerProfileLimit: 2,
};
@Injectable()
@ -355,6 +356,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
emojiPickerProfileLimit: calc('emojiPickerProfileLimit', vs => Math.max(...vs)),
};
}

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
<input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
<!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
<div ref="emojisEl" class="emojis" tabindex="-1">
@ -36,10 +36,15 @@ SPDX-License-Identifier: AGPL-3.0-only
</section>
<div v-if="tab === 'index'" class="group index">
<section v-if="showPinned && pinned.length > 0">
<section v-if="showPinned">
<div style="display: flex; ">
<div v-for="a in profileMax" :key="a" :title="defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`]" class="sllfktkhgl" :class="{ active: activeIndex === a || isDefaultProfile === a }" @click="pinnedProfileSelect(a)">
{{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }}
</div>
</div>
<div class="body">
<button
v-for="emoji in pinned"
v-for="emoji in pinnedEmojis"
:key="emoji"
:data-emoji="emoji"
class="_button item"
@ -95,7 +100,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button>
<button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
@ -117,8 +122,10 @@ import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js';
import { signinRequired } from '@/account.js';
import MkButton from '@/components/MkButton.vue';
import { deepClone } from '@/scripts/clone.js';
const $i = signinRequired();
const props = withDefaults(defineProps<{
showPinned?: boolean;
pinnedEmojis?: string[];
@ -133,7 +140,7 @@ const props = withDefaults(defineProps<{
const emit = defineEmits<{
(ev: 'chosen', v: string): void;
}>();
const profileMax = $i.policies.emojiPickerProfileLimit;
const searchEl = shallowRef<HTMLInputElement>();
const emojisEl = shallowRef<HTMLDivElement>();
@ -152,16 +159,15 @@ const q = ref<string>('');
const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
const pinnedEmojis = ref(pinned.value);
const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] };
function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
const parts = input.split('/').map(p => p.trim()); //
let currentNode: CustomEmojiFolderTree = root; // currentNode root
let includesPart = customEmojis.value.some(emoji => emoji.category !== null && emoji.category.includes(parts[0]+'/')) ;
console.log(includesPart)
let includesPart = customEmojis.value.some(emoji => emoji.category !== null && emoji.category.includes(parts[0] + '/'));
if (parts.length === 1 && parts[0] !== '' && includesPart) { // parts 1
parts.push(parts[0]) // parts parts[0] (test category test/test category )
parts.push(parts[0]); // parts parts[0] (test category test/test category )
}
for (const part of parts) { // parts
@ -237,7 +243,7 @@ watch(q, () => {
if (matches.size >= max) break;
}
}
if (matches.size >= max) return matches;for (const emoji of emojis) {
if (matches.size >= max) return matches; for (const emoji of emojis) {
if (emoji.name.startsWith(newQ)) {
matches.add(emoji);
if (matches.size >= max) break;
@ -410,6 +416,14 @@ function onEnter(ev: KeyboardEvent) {
done();
}
const activeIndex = ref(defaultStore.state.pickerProfileDefault);
pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${activeIndex.value > 1 ? activeIndex.value - 1 : ''}`]);
function pinnedProfileSelect(index:number) {
pinnedEmojis.value = props.asReactionPicker ? deepClone(defaultStore.state[`reactions${index > 1 ? index - 1 : ''}`]) : deepClone(defaultStore.state[`pinnedEmojis${index > 1 ? index - 1 : ''}`]);
activeIndex.value = index;
}
function done(query?: string): boolean | void {
if (query == null) query = q.value;
if (query == null || typeof query !== 'string') return;
@ -685,4 +699,24 @@ left: 0;*/
}
}
}
.sllfktkhgl{
display: inline-block;
padding: 0 4px;
font-size: 12px;
line-height: 32px;
text-align: center;
color: var(--fg);
cursor: pointer;
width: 100%;
transition: transform 0.3s ease;
box-shadow: 0 1.5px 0 var(--divider);
height: 32px;
overflow: hidden;
&:hover {
transform: translateY(1.5px);
}
&.active {
transform: translateY(5px);
}
}
</style>

View file

@ -68,8 +68,6 @@ import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { hostname } from '@/config.js';
import { multipleSelectUser } from '@/os.js';
const emit = defineEmits<{
(ev: 'ok', selected: Misskey.entities.UserDetailed): void;
(ev: 'cancel'): void;

View file

@ -99,6 +99,7 @@ export const ROLE_POLICIES = [
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
'emojiPickerProfileLimit'
] as const;
// なんか動かない

View file

@ -72,6 +72,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.emojiPickerProfileLimit, 'pickerProfileDefault'])">
<template #label>{{ i18n.ts._role._options.emojiPickerProfileLimit }}</template>
<template #suffix>{{ policies.emojiPickerProfileLimit }}</template>
<MkInput v-model="policies.emojiPickerProfileLimit" type="number">
</MkInput>
</MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.inviteLimit, 'inviteLimit'])">
<template #label>{{ i18n.ts._role._options.inviteLimit }}</template>
<template #suffix>{{ policies.inviteLimit }}</template>

View file

@ -5,6 +5,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps_m">
<MkSelect v-model="nowProfileId">
<template #label>{{ i18n.ts.emojiPickerProfile }}</template>
<option v-for="a in profileMax" :key="a" :value="a">{{ a }}. {{ defaultStore.state[`pickerProfileName${a > 1 ? a - 1 : ''}`] }} </option>
</MkSelect>
<MkInput v-model="profileName">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkFolder :defaultOpen="true">
<template #icon><i class="ti ti-pin"></i></template>
<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
@ -84,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
</MkFolder>
<MkButton inline primary @click="setDefaultProfile"> {{ i18n.ts.default }}</MkButton>
<FormSection>
<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
@ -139,6 +146,9 @@ import { emojiPicker } from '@/scripts/emoji-picker.js';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSelect from '@/components/MkSelect.vue';
import { signinRequired } from '@/account.js';
import MkInput from '@/components/MkInput.vue';
const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(defaultStore.state.reactions));
const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
@ -155,6 +165,20 @@ const setDefaultReaction = () => setDefault(pinnedEmojisForReaction);
const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev);
const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
const setDefaultEmoji = () => setDefault(pinnedEmojis);
const nowProfileId = ref(defaultStore.state.pickerProfileDefault);
const $i = signinRequired();
const profileMax = $i.policies.emojiPickerProfileLimit;
const profileName = ref(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
pinnedEmojisForReaction.value = deepClone(defaultStore.state[`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
pinnedEmojis.value = deepClone(defaultStore.state[`pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
profileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
watch(nowProfileId, () => {
pinnedEmojisForReaction.value = deepClone(defaultStore.state[`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
pinnedEmojis.value = deepClone(defaultStore.state[`pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
profileName.value = deepClone(defaultStore.state[`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`]);
});
function previewReaction(ev: MouseEvent) {
reactionPicker.show(getHTMLElement(ev));
@ -226,17 +250,32 @@ function getHTMLElement(ev: MouseEvent): HTMLElement {
}
watch(pinnedEmojisForReaction, () => {
defaultStore.set('reactions', pinnedEmojisForReaction.value);
defaultStore.set(`reactions${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, pinnedEmojisForReaction.value);
}, {
deep: true,
});
watch(pinnedEmojis, () => {
defaultStore.set('pinnedEmojis', pinnedEmojis.value);
defaultStore.set( `pinnedEmojis${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, pinnedEmojis.value);
}, {
deep: true,
});
watch(profileName, () => {
defaultStore.set(`pickerProfileName${nowProfileId.value > 1 ? nowProfileId.value - 1 : ''}`, profileName.value);
}, {
deep: true,
});
async function setDefaultProfile() {
const { canceled } = await os.confirm({
type: 'info',
text: i18n.ts.setDefaultProfileConfirm,
});
if (canceled) return;
await defaultStore.set('pickerProfileDefault', nowProfileId.value);
}
definePageMetadata({
title: i18n.ts.emojiPicker,
icon: 'ti ti-mood-happy',

View file

@ -58,11 +58,10 @@ export const noteActions: NoteAction[] = [];
export const noteViewInterruptors: NoteViewInterruptor[] = [];
export const notePostInterruptors: NotePostInterruptor[] = [];
export const pageViewInterruptors: PageViewInterruptor[] = [];
export const bannerDark='https://files.prismisskey.space/misskey/e088c6d1-b07f-4312-8d41-fee2f64071e9.png'
export const bannerLight ='https://files.prismisskey.space/misskey/85500d2f-41a9-48ff-a737-65d6fdf74604.png'
export const iconDark='https://files.prismisskey.space/misskey/484efc68-de41-4786-b2b6-e5085c31c2c4.webp'
export const iconLight='https://files.prismisskey.space/misskey/c3d722fe-379f-4c85-9414-90c232d53237.webp'
export const bannerDark = 'https://files.prismisskey.space/misskey/e088c6d1-b07f-4312-8d41-fee2f64071e9.png';
export const bannerLight = 'https://files.prismisskey.space/misskey/85500d2f-41a9-48ff-a737-65d6fdf74604.png';
export const iconDark = 'https://files.prismisskey.space/misskey/484efc68-de41-4786-b2b6-e5085c31c2c4.webp';
export const iconLight = 'https://files.prismisskey.space/misskey/c3d722fe-379f-4c85-9414-90c232d53237.webp';
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
@ -129,6 +128,74 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'account',
default: [],
},
reactions1: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis1: {
where: 'account',
default: [],
},
reactions2: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis2: {
where: 'account',
default: [],
},
reactions3: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis3: {
where: 'account',
default: [],
},
reactions4: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis4: {
where: 'account',
default: [],
},
reactions5: {
where: 'account',
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
},
pinnedEmojis5: {
where: 'account',
default: [],
},
pickerProfileName: {
where: 'account',
default: 'default',
},
pickerProfileName1: {
where: 'account',
default: '1',
},
pickerProfileName2: {
where: 'account',
default: '2',
},
pickerProfileName3: {
where: 'account',
default: '3',
},
pickerProfileName4: {
where: 'account',
default: '4',
},
pickerProfileName5: {
where: 'account',
default: '5',
},
pickerProfileDefault: {
where: 'account',
default: 1,
},
reactionAcceptance: {
where: 'account',
default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
@ -302,13 +369,13 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 'true',
},
bannerUrl:{
bannerUrl: {
where: 'device',
default: bannerDark
default: bannerDark,
},
iconUrl:{
iconUrl: {
where: 'device',
default: iconDark
default: iconDark,
},
instanceTicker: {
where: 'device',
@ -334,9 +401,9 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: [] as string[],
},
enablehanntenn:{
where:'device',
default: false
enablehanntenn: {
where: 'device',
default: false,
},
recentlyUsedUsers: {
where: 'device',
@ -378,39 +445,39 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 3,
},
specifiedColor:{
specifiedColor: {
where: 'device',
default: '#FFFF64',
},
followerColor:{
followerColor: {
where: 'device',
default: '#FF00FF',
},
homeColor:{
homeColor: {
where: 'device',
default: '#00FFFF',
},
localOnlyColor:{
where:'device',
default: '#2b2c41'
localOnlyColor: {
where: 'device',
default: '#2b2c41',
},
numberOfGamingSpeed: {
where: 'device',
default: 44,
},
onlyAndWithSave:{
onlyAndWithSave: {
where: 'device',
default: false,
},
onlyFiles:{
onlyFiles: {
where: 'device',
default: false,
},
withReplies:{
withReplies: {
where: 'device',
default: true,
},
withRenotes:{
withRenotes: {
where: 'device',
default: true,
},
@ -422,15 +489,15 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
showMediaTimeline:{
showMediaTimeline: {
where: 'device',
default: true,
},
showGlobalTimeline:{
showGlobalTimeline: {
where: 'device',
default: true,
},
showVisibilityColor:{
showVisibilityColor: {
where: 'device',
default: false,
},
@ -557,7 +624,6 @@ export const defaultStore = markRaw(new Storage('base', {
},
}));
// TODO: 他のタブと永続化されたstateを同期
const PREFIX = 'miux:' as const;