Unify context menu and fix mute/unmute and report actions

This commit is contained in:
Bojan Mojsilovic 2024-03-18 12:57:36 +01:00
parent 1fe6b2851b
commit 29461e4e96
10 changed files with 269 additions and 122 deletions

View File

@ -19,6 +19,7 @@ import Landing from '../../pages/Landing';
import ReactionsModal from '../ReactionsModal/ReactionsModal';
import { useAppContext } from '../../contexts/AppContext';
import CustomZap from '../CustomZap/CustomZap';
import NoteContextMenu from '../Note/NoteContextMenu';
export const [isHome, setIsHome] = createSignal(false);
@ -183,6 +184,11 @@ const Layout: Component = () => {
</div>
</div>
</div>
<NoteContextMenu
open={app?.showNoteContextMenu}
onClose={app?.actions.closeContextMenu}
data={app?.noteContextMenuInfo}
/>
</>
</Show>
)

View File

@ -185,16 +185,7 @@
}
}
.contextMenu {
position: relative;
width: 16px;
height: 32px;
display: flex;
align-items: center;
text-align: center;
font-weight: bold;
.contextButton {
.contextButton {
width: 16px;
height: 32px;
padding: 0;
@ -224,7 +215,16 @@
background-color: var(--text-primary);
}
}
}
}
.contextMenu {
position: absolute;
width: 16px;
height: 32px;
display: flex;
align-items: center;
text-align: center;
font-weight: bold;
}

View File

@ -22,11 +22,13 @@ import { reportUser } from '../../lib/profile';
import { useToastContext } from '../Toaster/Toaster';
import { broadcastEvent } from '../../lib/notes';
import { getScreenCordinates } from '../../utils';
import { NoteContextMenuInfo } from '../../contexts/AppContext';
import ConfirmModal from '../ConfirmModal/ConfirmModal';
const NoteContextMenu: Component<{
note: PrimalNote,
openCustomZap?: () => void;
openReactions?: () => void,
data: NoteContextMenuInfo,
open: boolean,
onClose: () => void,
id?: string,
}> = (props) => {
const account = useAccountContext();
@ -37,62 +39,95 @@ const NoteContextMenu: Component<{
const [confirmReportUser, setConfirmReportUser] = createSignal(false);
const [confirmMuteUser, setConfirmMuteUser] = createSignal(false);
const openContextMenu = (e: MouseEvent) => {
e.preventDefault();
setContext(true);
const [orientation, setOrientation] = createSignal<'down' | 'up'>('down')
const note = () => props.data?.note;
const position = () => {
return props.data?.position;
};
const openCustomZap = props.data?.openCustomZap || (() => {});
const openReactions = props.data?.openReactions || (() => {});
createEffect(() => {
if(!context) return;
if (!props.open) {
context.setAttribute('style',`top: -1024px; left: -1034px;`);
}
const docRect = document.documentElement.getBoundingClientRect();
const pos = {
top: (Math.floor(position()?.top || 0) - docRect.top),
left: (Math.floor(position()?.left || 0)),
}
context.setAttribute('style',`top: ${pos.top + 12}px; left: ${pos.left + 12}px;`);
const height = 440;
const orient = Math.floor(position()?.bottom || 0) + height < window.innerHeight ? 'down' : 'up';
setOrientation(() => orient);
});
const doMuteUser = () => {
account?.actions.addToMuteList(props.note.post.pubkey);
account?.actions.addToMuteList(note()?.post.pubkey);
props.onClose();
};
const doUnmuteUser = () => {
account?.actions.removeFromMuteList(props.note.post.pubkey);
account?.actions.removeFromMuteList(note()?.post.pubkey);
props.onClose();
};
const doReportUser = () => {
reportUser(props.note.user.pubkey, `report_user_${APP_ID}`, props.note.user);
setContext(false);
toaster?.sendSuccess(intl.formatMessage(tToast.noteAuthorReported, { name: userName(props.note.user)}));
reportUser(note()?.user.pubkey, `report_user_${APP_ID}`, note()?.user);
props.onClose();
toaster?.sendSuccess(intl.formatMessage(tToast.noteAuthorReported, { name: userName(note()?.user)}));
};
const copyNoteLink = () => {
navigator.clipboard.writeText(`${window.location.origin}/e/${props.note.post.noteId}`);
setContext(false);
if (!props.data) return;
navigator.clipboard.writeText(`${window.location.origin}/e/${note().post.noteId}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.notePrimalLinkCoppied));
};
const copyNoteText = () => {
navigator.clipboard.writeText(`${props.note.post.content}`);
setContext(false);
if (!props.data) return;
navigator.clipboard.writeText(`${note().post.content}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.notePrimalTextCoppied));
};
const copyNoteId = () => {
navigator.clipboard.writeText(`${props.note.post.noteId}`);
setContext(false);
if (!props.data) return;
navigator.clipboard.writeText(`${note().post.noteId}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.noteIdCoppied));
};
const copyRawData = () => {
navigator.clipboard.writeText(`${JSON.stringify(props.note.msg)}`);
setContext(false);
if (!props.data) return;
navigator.clipboard.writeText(`${JSON.stringify(note().msg)}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.noteRawDataCoppied));
};
const copyUserNpub = () => {
navigator.clipboard.writeText(`${props.note.user.npub}`);
setContext(false);
if (!props.data) return;
navigator.clipboard.writeText(`${note().user.npub}`);
props.onClose()
toaster?.sendSuccess(intl.formatMessage(tToast.noteAuthorNpubCoppied));
};
const broadcastNote = async () => {
if (!account) {
if (!account || !props.data) {
return;
}
const { success } = await broadcastEvent(props.note.msg as NostrRelaySignedEvent, account?.relays, account?.relaySettings);
setContext(false);
const { success } = await broadcastEvent(note().msg as NostrRelaySignedEvent, account?.relays, account?.relaySettings);
props.onClose()
if (success) {
toaster?.sendSuccess(intl.formatMessage(tToast.noteBroadcastSuccess));
@ -103,14 +138,15 @@ const NoteContextMenu: Component<{
const onClickOutside = (e: MouseEvent) => {
if (
!document?.getElementById(`note_context_${props.note.post.id}`)?.contains(e.target as Node)
!props.data ||
!document?.getElementById(`note_context_${note().post.id}`)?.contains(e.target as Node)
) {
setContext(false);
props.onClose()
}
}
createEffect(() => {
if (showContext()) {
if (props.open) {
document.addEventListener('click', onClickOutside);
}
else {
@ -119,24 +155,24 @@ const NoteContextMenu: Component<{
});
const isVerifiedByPrimal = () => {
return !!props.note.user.nip05 &&
props.note.user.nip05.endsWith('primal.net');
return props.data && !!note().user.nip05 &&
note().user.nip05.endsWith('primal.net');
}
const noteContextForEveryone: MenuItem[] = [
{
label: intl.formatMessage(tActions.noteContext.reactions),
action: () => {
props.openReactions && props.openReactions();
setContext(false);
openReactions();
props.onClose()
},
icon: 'heart',
},
{
label: intl.formatMessage(tActions.noteContext.zap),
action: () => {
props.openCustomZap && props.openCustomZap();
setContext(false);
openCustomZap();
props.onClose()
},
icon: 'feed_zap',
},
@ -172,12 +208,15 @@ const NoteContextMenu: Component<{
},
];
const noteContextForOtherPeople: MenuItem[] = [
const noteContextForOtherPeople: () => MenuItem[] = () => {
const isMuted = account?.muted.includes(note()?.user.pubkey);
return [
{
label: intl.formatMessage(tActions.noteContext.muteAuthor),
label: isMuted ? intl.formatMessage(tActions.noteContext.unmuteAuthor) : intl.formatMessage(tActions.noteContext.muteAuthor),
action: () => {
setConfirmMuteUser(true);
setContext(false);
isMuted ? doUnmuteUser() : setConfirmMuteUser(true);
props.onClose()
},
icon: 'mute_user',
warning: true,
@ -186,39 +225,48 @@ const NoteContextMenu: Component<{
label: intl.formatMessage(tActions.noteContext.reportAuthor),
action: () => {
setConfirmReportUser(true);
setContext(false);
props.onClose()
},
icon: 'report',
warning: true,
},
];
};
const noteContext = account?.publicKey !== props.note.post.pubkey ?
[ ...noteContextForEveryone, ...noteContextForOtherPeople] :
const noteContext = () => account?.publicKey !== note()?.post.pubkey ?
[ ...noteContextForEveryone, ...noteContextForOtherPeople()] :
noteContextForEveryone;
let context: HTMLDivElement | undefined;
const determineOrient = () => {
const coor = getScreenCordinates(context);
const height = 440;
return (coor.y || 0) + height < window.innerHeight + window.scrollY ? 'down' : 'up';
}
return (
<div class={styles.contextMenu} ref={context}>
<button
class={styles.contextButton}
onClick={openContextMenu}
>
<div class={styles.contextIcon} ></div>
</button>
<ConfirmModal
open={confirmReportUser()}
description={intl.formatMessage(tActions.reportUserConfirm, { name: authorName(note()?.user) })}
onConfirm={() => {
doReportUser();
setConfirmReportUser(false);
}}
onAbort={() => setConfirmReportUser(false)}
/>
<ConfirmModal
open={confirmMuteUser()}
description={intl.formatMessage(tActions.muteUserConfirm, { name: authorName(note()?.user) })}
onConfirm={() => {
doMuteUser();
setConfirmMuteUser(false);
}}
onAbort={() => setConfirmMuteUser(false)}
/>
<PrimalMenu
id={`note_context_${props.note.post.id}`}
items={noteContext}
hidden={!showContext()}
id={`note_context_${note()?.post.id}`}
items={noteContext()}
hidden={!props.open}
position="note_footer"
orientation={determineOrient()}
orientation={orientation()}
/>
</div>
)

View File

@ -9,6 +9,38 @@
align-items: center;
}
.contextButton {
width: 48px;
height: 32px;
padding: 0;
margin: 0;
background: none;
border: none;
outline: none;
display: flex;
justify-content: center;
align-items: center;
&:focus {
outline: none;
box-shadow: none;
}
.contextIcon {
width: 16px;
height: 14px;
background-color: var(--text-secondary-2);
-webkit-mask: url(../../../assets/icons/context.svg) no-repeat 0 / 100%;
mask: url(../../../assets/icons/context.svg) no-repeat 0 / 100%;
}
&:hover {
.contextIcon {
background-color: var(--text-primary);
}
}
}
.footer {
display: grid;
grid-template-columns: 128px 128px 128px 128px 16px;

View File

@ -46,6 +46,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
const [isRepostMenuVisible, setIsRepostMenuVisible] = createSignal(false);
let footerDiv: HTMLDivElement | undefined;
let noteContextMenu: HTMLDivElement | undefined;
const repostMenuItems: MenuItem[] = [
{
@ -374,7 +375,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
}
return (
<div id={props.id} class={`${styles.footer} ${props.wide ? styles.wide : ''}`} ref={footerDiv}>
<div id={props.id} class={`${styles.footer} ${props.wide ? styles.wide : ''}`} ref={footerDiv} onClick={(e) => {e.preventDefault();}}>
<Show when={showZapAnim()}>
<ZapAnimation
@ -442,21 +443,32 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
</div>
</button>
<div class={styles.context}>
<NoteContextMenu
note={props.note}
openCustomZap={() => {
<div ref={noteContextMenu} class={styles.context}>
<button
class={styles.contextButton}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
app?.actions.openContextMenu(
props.note,
noteContextMenu?.getBoundingClientRect(),
() => {
app?.actions.openCustomZapModal(customZapInfo);
}}
openReactions={() => {
},
() => {
app?.actions.openReactionModal(props.note.post.id, {
likes: likes(),
zaps: zapCount(),
reposts: reposts(),
quotes: 0,
});
}
);
}}
/>
>
<div class={styles.contextIcon} ></div>
</button>
</div>
</div>
)

View File

@ -32,6 +32,8 @@ const ProfileTabs: Component<{
const profile = useProfileContext();
const account = useAccountContext();
const [currentTab, setCurrentTab] = createSignal<string>('notes');
const addToAllowlist = async () => {
const pk = profile?.profileKey;
if (pk) {
@ -58,7 +60,10 @@ const ProfileTabs: Component<{
return;
}
account.actions.removeFromMuteList(pk);
account.actions.removeFromMuteList(pk, () => {
props.setProfile && props.setProfile(pk);
onChangeValue(currentTab());
});
};
const onContactAction = (remove: boolean, pubkey: string) => {
@ -125,6 +130,8 @@ const ProfileTabs: Component<{
const onChangeValue = (value: string) => {
if (!profile) return;
setCurrentTab(() => value);
switch(value) {
case 'notes':
profile.notes.length === 0 && profile.actions.fetchNotes(profile.profileKey);

View File

@ -25,6 +25,13 @@ export type CustomZapInfo = {
onCancel: (zapOption: ZapOption) => void,
};
export type NoteContextMenuInfo = {
note: PrimalNote,
position: DOMRect | undefined,
openCustomZap?: () => void,
openReactions?: () => void,
};
export type AppContextStore = {
isInactive: boolean,
appState: 'sleep' | 'waking' | 'woke',
@ -32,11 +39,15 @@ export type AppContextStore = {
reactionStats: ReactionStats,
showCustomZapModal: boolean,
customZap: CustomZapInfo | undefined,
showNoteContextMenu: boolean,
noteContextMenuInfo: NoteContextMenuInfo | undefined,
actions: {
openReactionModal: (noteId: string, stats: ReactionStats) => void,
closeReactionModal: () => void,
openCustomZapModal: (custonZapInfo: CustomZapInfo) => void,
closeCustomZapModal: () => void,
openContextMenu: (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => void,
closeContextMenu: () => void,
},
}
@ -52,6 +63,8 @@ const initialData: Omit<AppContextStore, 'actions'> = {
},
showCustomZapModal: false,
customZap: undefined,
showNoteContextMenu: false,
noteContextMenuInfo: undefined,
};
export const AppContext = createContext<AppContextStore>();
@ -96,6 +109,20 @@ export const AppProvider = (props: { children: JSXElement }) => {
updateStore('showCustomZapModal', () => false);
};
const openContextMenu = (note: PrimalNote, position: DOMRect | undefined, openCustomZapModal: () => void, openReactionModal: () => void) => {
updateStore('noteContextMenuInfo', reconcile({
note,
position,
openCustomZapModal,
openReactionModal,
}))
updateStore('showNoteContextMenu', () => true);
};
const closeContextMenu = () => {
updateStore('showNoteContextMenu', () => false);
};
// EFFECTS --------------------------------------
onMount(() => {
@ -138,6 +165,8 @@ export const AppProvider = (props: { children: JSXElement }) => {
closeReactionModal,
openCustomZapModal,
closeCustomZapModal,
openContextMenu,
closeContextMenu,
}
});

View File

@ -128,6 +128,7 @@ export type ProfileContextStore = {
fetchFollowerList: (pubkey: string | undefined) => void,
fetchRelayList: (pubkey: string | undefined) => void,
clearContacts: () => void,
clearFilterReason: () => void,
removeContact: (pubkey: string) => void,
addContact: (pubkey: string, source: PrimalUser[]) => void,
fetchZapList: (pubkey: string | undefined) => void,
@ -514,6 +515,10 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
updateStore('profileStats', () => ({}));
};
const clearFilterReason = () => {
updateStore('filterReason', () => null);
};
const fetchNextPage = () => {
const lastNote = store.notes[store.notes.length - 1];
@ -1018,6 +1023,8 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
if (reason?.action === 'block') {
updateStore('filterReason', () => ({ ...reason }));
} else {
updateStore('filterReason', () => null);
}
}
@ -1229,6 +1236,7 @@ export const ProfileProvider = (props: { children: ContextChildren }) => {
fetchNextZapsPage,
clearZaps,
resetProfile,
clearFilterReason,
},
});

View File

@ -121,6 +121,7 @@ const Profile: Component = () => {
profile?.actions.clearReplies();
profile?.actions.clearContacts();
profile?.actions.clearZaps();
profile?.actions.clearFilterReason();
}
let keyIsDone = false
@ -416,7 +417,6 @@ const Profile: Component = () => {
toaster?.sendSuccess(intl.formatMessage(tToast.noteAuthorReported, { name: userName(profile?.userProfile)}));
};
const addToAllowlist = async () => {
const pk = getHex();
if (pk) {
@ -679,7 +679,7 @@ const Profile: Component = () => {
</Show>
</div>
<ProfileTabs />
<ProfileTabs setProfile={setProfile} />
<ConfirmModal
open={confirmReportUser()}

View File

@ -405,6 +405,11 @@ export const actions = {
defaultMessage: 'Mute user',
description: 'Label for muting user from context menu',
},
unmuteAuthor: {
id: 'actions.noteContext.unmuteAuthor',
defaultMessage: 'Unmute user',
description: 'Label for unmuting user from context menu',
},
reportAuthor: {
id: 'actions.noteContext.reportAuthor',
defaultMessage: 'Report user',