feat: add option to boost with Home and Followers-only visibility (#9788)

Closes: #9777

This pull request includes UI changes (please check the attached images).

Co-authored-by: naskya <m@naskya.net>
Reviewed-on: https://codeberg.org/calckey/calckey/pulls/9788
Co-authored-by: naskya <naskya@noreply.codeberg.org>
Co-committed-by: naskya <naskya@noreply.codeberg.org>
This commit is contained in:
naskya 2023-04-01 06:53:29 +00:00 committed by Kainoa Kanter
parent f70c5da0bd
commit 0e8fe41aaa
5 changed files with 134 additions and 26 deletions

View file

@ -96,6 +96,9 @@ unfollow: "Unfollow"
followRequestPending: "Follow request pending" followRequestPending: "Follow request pending"
enterEmoji: "Enter an emoji" enterEmoji: "Enter an emoji"
renote: "Boost" renote: "Boost"
renoteAsUnlisted: "Boost (Unlisted)"
renoteToFollowers: "Boost (Followers)"
renoteToRecipients: "Boost (Recipients)"
unrenote: "Take back boost" unrenote: "Take back boost"
renoted: "Boosted." renoted: "Boosted."
cantRenote: "This post can't be boosted." cantRenote: "This post can't be boosted."

View file

@ -96,6 +96,9 @@ unfollow: "フォロー解除"
followRequestPending: "フォロー許可待ち" followRequestPending: "フォロー許可待ち"
enterEmoji: "絵文字を入力" enterEmoji: "絵文字を入力"
renote: "ブースト" renote: "ブースト"
renoteAsUnlisted: "ホームにブースト"
renoteToFollowers: "フォロワー限定でブースト"
renoteToRecipients: "宛先のユーザーにブースト"
unrenote: "ブースト解除" unrenote: "ブースト解除"
renoted: "ブーストしました。" renoted: "ブーストしました。"
cantRenote: "この投稿はブーストできません。" cantRenote: "この投稿はブーストできません。"

View file

@ -10,20 +10,26 @@
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item === null" class="divider"></div> <div v-if="item === null" class="divider"></div>
<span v-else-if="item.type === 'label'" class="label item"> <span v-else-if="item.type === 'label'" class="label item">
<span>{{ item.text }}</span> <span :style="item.textStyle || ''">{{ item.text }}</span>
</span> </span>
<span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item"> <span v-else-if="item.type === 'pending'" :tabindex="i" class="pending item">
<span><MkEllipsis/></span> <span><MkEllipsis/></span>
</span> </span>
<MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkA v-else-if="item.type === 'link'" :to="item.to" :tabindex="i" class="_button item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i> <i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<span v-else-if="item.icons">
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
</span>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span> <span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span> <span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</MkA> </MkA>
<a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <a v-else-if="item.type === 'a'" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i> <i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<span>{{ item.text }}</span> <span v-else-if="item.icons">
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span> <span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</a> </a>
<button v-else-if="item.type === 'user' && !items.hidden" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <button v-else-if="item.type === 'user' && !items.hidden" :tabindex="i" class="_button item" :class="{ active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
@ -31,17 +37,23 @@
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span> <span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</button> </button>
<span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <span v-else-if="item.type === 'switch'" :tabindex="i" class="item" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch">{{ item.text }}</FormSwitch> <FormSwitch v-model="item.ref" :disabled="item.disabled" class="form-switch" :style="item.textStyle || ''">{{ item.text }}</FormSwitch>
</span> </span>
<button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)"> <button v-else-if="item.type === 'parent'" :tabindex="i" class="_button item parent" :class="{ childShowing: childShowingItem === item }" @mouseenter="showChildren(item, $event)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i> <i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<span>{{ item.text }}</span> <span v-else-if="item.icons">
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
</span>
<span :style="item.textStyle || ''">{{ item.text }}</span>
<span class="caret"><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i></span> <span class="caret"><i class="ph-caret-right ph-bold ph-lg ph-fw ph-lg"></i></span>
</button> </button>
<button v-else-if="!item.hidden" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <button v-else-if="!item.hidden" :tabindex="i" class="_button item" :class="{ danger: item.danger, active: item.active }" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i> <i v-if="item.icon" class="ph-fw ph-lg" :class="item.icon"></i>
<span v-else-if="item.icons">
<i v-for="icon in item.icons" class="ph-fw ph-lg" :class="icon"></i>
</span>
<MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" class="avatar"/>
<span>{{ item.text }}</span> <span :style="item.textStyle || ''">{{ item.text }}</span>
<span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span> <span v-if="item.indicate" class="indicator"><i class="ph-circle ph-fill"></i></span>
</button> </button>
</template> </template>

View file

@ -64,24 +64,91 @@ const renote = async (viaKeyboard = false, ev?: MouseEvent) => {
const users = renotes.map(x => x.user.id); const users = renotes.map(x => x.user.id);
const hasRenotedBefore = users.includes($i.id); const hasRenotedBefore = users.includes($i.id);
let buttonActions = [{ let buttonActions = [];
text: i18n.ts.renote,
icon: 'ph-repeat ph-bold ph-lg', if (props.note.visibility === 'public') {
danger: false, buttonActions.push({
action: () => { text: i18n.ts.renote,
os.api('notes/create', { textStyle: 'font-weight: bold',
renoteId: props.note.id, icon: 'ph-repeat ph-bold ph-lg',
visibility: props.note.visibility, danger: false,
}); action: () => {
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; os.api('notes/create', {
if (el) { renoteId: props.note.id,
const rect = el.getBoundingClientRect(); visibility: 'public',
const x = rect.left + (el.offsetWidth / 2); });
const y = rect.top + (el.offsetHeight / 2); const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
os.popup(Ripple, { x, y }, {}, 'end'); if (el) {
} const rect = el.getBoundingClientRect();
}, const x = rect.left + (el.offsetWidth / 2);
}]; const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
},
});
}
if (['public', 'home'].includes(props.note.visibility)) {
buttonActions.push({
text: i18n.ts.renoteAsUnlisted,
icons: ['ph-repeat ph-bold ph-lg', 'ph-house ph-bold ph-lg'],
danger: false,
action: () => {
os.api('notes/create', {
renoteId: props.note.id,
visibility: 'home',
});
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
},
});
}
if (props.note.visibility === 'specified') {
buttonActions.push({
text: i18n.ts.renoteToRecipients,
icons: ['ph-repeat ph-bold ph-lg', 'ph-envelope-simple-open ph-bold ph-lg'],
danger: false,
action: () => {
os.api('notes/create', {
renoteId: props.note.id,
visibility: 'specified',
visibleUserIds: props.note.visibleUserIds,
});
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
},
});
} else {
buttonActions.push({
text: i18n.ts.renoteToFollowers,
icons: ['ph-repeat ph-bold ph-lg', 'ph-lock-simple-open ph-bold ph-lg'],
danger: false,
action: () => {
os.api('notes/create', {
renoteId: props.note.id,
visibility: 'followers',
});
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) {
const rect = el.getBoundingClientRect();
const x = rect.left + (el.offsetWidth / 2);
const y = rect.top + (el.offsetHeight / 2);
os.popup(Ripple, { x, y }, {}, 'end');
}
},
});
}
if (!defaultStore.state.seperateRenoteQuote) { if (!defaultStore.state.seperateRenoteQuote) {
buttonActions.push({ buttonActions.push({

View file

@ -5,11 +5,16 @@ export type MenuAction = (ev: MouseEvent) => void;
export type MenuDivider = null; export type MenuDivider = null;
export type MenuNull = undefined; export type MenuNull = undefined;
export type MenuLabel = { type: "label"; text: string }; export type MenuLabel = {
type: "label";
text: string;
textStyle?: string;
};
export type MenuLink = { export type MenuLink = {
type: "link"; type: "link";
to: string; to: string;
text: string; text: string;
textStyle?: string;
icon?: string; icon?: string;
indicate?: boolean; indicate?: boolean;
avatar?: Misskey.entities.User; avatar?: Misskey.entities.User;
@ -20,6 +25,7 @@ export type MenuA = {
target?: string; target?: string;
download?: string; download?: string;
text: string; text: string;
textStyle?: string;
icon?: string; icon?: string;
indicate?: boolean; indicate?: boolean;
}; };
@ -35,11 +41,13 @@ export type MenuSwitch = {
type: "switch"; type: "switch";
ref: Ref<boolean>; ref: Ref<boolean>;
text: string; text: string;
textStyle?: string;
disabled?: boolean; disabled?: boolean;
}; };
export type MenuButton = { export type MenuButton = {
type?: "button"; type?: "button";
text: string; text: string;
textStyle?: string;
icon?: string; icon?: string;
indicate?: boolean; indicate?: boolean;
danger?: boolean; danger?: boolean;
@ -48,9 +56,22 @@ export type MenuButton = {
avatar?: Misskey.entities.User; avatar?: Misskey.entities.User;
action: MenuAction; action: MenuAction;
}; };
export type MenuButtonMultipleIcons = {
type?: "button";
text: string;
textStyle?: string;
icons: string[];
indicate?: boolean;
danger?: boolean;
active?: boolean;
hidden?: boolean;
avatar?: Misskey.entities.User;
action: MenuAction;
};
export type MenuParent = { export type MenuParent = {
type: "parent"; type: "parent";
text: string; text: string;
textStyle?: string;
icon?: string; icon?: string;
children: OuterMenuItem[]; children: OuterMenuItem[];
}; };
@ -66,9 +87,10 @@ type OuterMenuItem =
| MenuUser | MenuUser
| MenuSwitch | MenuSwitch
| MenuButton | MenuButton
| MenuButtonMultipleIcons
| MenuParent; | MenuParent;
type OuterPromiseMenuItem = Promise< type OuterPromiseMenuItem = Promise<
MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuParent MenuLabel | MenuLink | MenuA | MenuUser | MenuSwitch | MenuButton | MenuButtonMultipleIcons | MenuParent
>; >;
export type MenuItem = OuterMenuItem | OuterPromiseMenuItem; export type MenuItem = OuterMenuItem | OuterPromiseMenuItem;
export type InnerMenuItem = export type InnerMenuItem =
@ -80,4 +102,5 @@ export type InnerMenuItem =
| MenuUser | MenuUser
| MenuSwitch | MenuSwitch
| MenuButton | MenuButton
| MenuButtonMultipleIcons
| MenuParent; | MenuParent;