From 29461e4e962b24aa01ac7695b79a3c5a4ed1bb4a Mon Sep 17 00:00:00 2001 From: Bojan Mojsilovic Date: Mon, 18 Mar 2024 12:57:36 +0100 Subject: [PATCH] Unify context menu and fix mute/unmute and report actions --- src/components/Layout/Layout.tsx | 6 + src/components/Note/Note.module.scss | 66 +++--- src/components/Note/NoteContextMenu.tsx | 190 +++++++++++------- .../Note/NoteFooter/NoteFooter.module.scss | 32 +++ src/components/Note/NoteFooter/NoteFooter.tsx | 42 ++-- src/components/ProfileTabs/ProfileTabs.tsx | 9 +- src/contexts/AppContext.tsx | 29 +++ src/contexts/ProfileContext.tsx | 8 + src/pages/Profile.tsx | 4 +- src/translations.ts | 5 + 10 files changed, 269 insertions(+), 122 deletions(-) diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 8db76b5..09f97b4 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -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 = () => { + ) diff --git a/src/components/Note/Note.module.scss b/src/components/Note/Note.module.scss index 2ed4470..5d1eef1 100644 --- a/src/components/Note/Note.module.scss +++ b/src/components/Note/Note.module.scss @@ -185,46 +185,46 @@ } } +.contextButton { + width: 16px; + 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); + } + } +} + .contextMenu { - position: relative; + position: absolute; width: 16px; height: 32px; display: flex; align-items: center; text-align: center; font-weight: bold; - - .contextButton { - width: 16px; - 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); - } - } - } } diff --git a/src/components/Note/NoteContextMenu.tsx b/src/components/Note/NoteContextMenu.tsx index 0662e08..6afa326 100644 --- a/src/components/Note/NoteContextMenu.tsx +++ b/src/components/Note/NoteContextMenu.tsx @@ -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,53 +208,65 @@ const NoteContextMenu: Component<{ }, ]; - const noteContextForOtherPeople: MenuItem[] = [ - { - label: intl.formatMessage(tActions.noteContext.muteAuthor), - action: () => { - setConfirmMuteUser(true); - setContext(false); - }, - icon: 'mute_user', - warning: true, - }, - { - label: intl.formatMessage(tActions.noteContext.reportAuthor), - action: () => { - setConfirmReportUser(true); - setContext(false); - }, - icon: 'report', - warning: true, - }, - ]; + const noteContextForOtherPeople: () => MenuItem[] = () => { + const isMuted = account?.muted.includes(note()?.user.pubkey); - const noteContext = account?.publicKey !== props.note.post.pubkey ? - [ ...noteContextForEveryone, ...noteContextForOtherPeople] : + return [ + { + label: isMuted ? intl.formatMessage(tActions.noteContext.unmuteAuthor) : intl.formatMessage(tActions.noteContext.muteAuthor), + action: () => { + isMuted ? doUnmuteUser() : setConfirmMuteUser(true); + props.onClose() + }, + icon: 'mute_user', + warning: true, + }, + { + label: intl.formatMessage(tActions.noteContext.reportAuthor), + action: () => { + setConfirmReportUser(true); + props.onClose() + }, + icon: 'report', + warning: true, + }, + ]; + }; + + 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 (
- + { + doReportUser(); + setConfirmReportUser(false); + }} + onAbort={() => setConfirmReportUser(false)} + /> + + { + doMuteUser(); + setConfirmMuteUser(false); + }} + onAbort={() => setConfirmMuteUser(false)} + /> +
) diff --git a/src/components/Note/NoteFooter/NoteFooter.module.scss b/src/components/Note/NoteFooter/NoteFooter.module.scss index 1aaa858..e491d49 100644 --- a/src/components/Note/NoteFooter/NoteFooter.module.scss +++ b/src/components/Note/NoteFooter/NoteFooter.module.scss @@ -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; diff --git a/src/components/Note/NoteFooter/NoteFooter.tsx b/src/components/Note/NoteFooter/NoteFooter.tsx index cafc0d5..c19ce42 100644 --- a/src/components/Note/NoteFooter/NoteFooter.tsx +++ b/src/components/Note/NoteFooter/NoteFooter.tsx @@ -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 ( -
+
{e.preventDefault();}}> =
-
- { - app?.actions.openCustomZapModal(customZapInfo); +
+
) diff --git a/src/components/ProfileTabs/ProfileTabs.tsx b/src/components/ProfileTabs/ProfileTabs.tsx index 2a9408c..8d72d2a 100644 --- a/src/components/ProfileTabs/ProfileTabs.tsx +++ b/src/components/ProfileTabs/ProfileTabs.tsx @@ -32,6 +32,8 @@ const ProfileTabs: Component<{ const profile = useProfileContext(); const account = useAccountContext(); + const [currentTab, setCurrentTab] = createSignal('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); diff --git a/src/contexts/AppContext.tsx b/src/contexts/AppContext.tsx index d8d2749..3516eb3 100644 --- a/src/contexts/AppContext.tsx +++ b/src/contexts/AppContext.tsx @@ -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 = { }, showCustomZapModal: false, customZap: undefined, + showNoteContextMenu: false, + noteContextMenuInfo: undefined, }; export const AppContext = createContext(); @@ -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, } }); diff --git a/src/contexts/ProfileContext.tsx b/src/contexts/ProfileContext.tsx index e702557..c64c711 100644 --- a/src/contexts/ProfileContext.tsx +++ b/src/contexts/ProfileContext.tsx @@ -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, }, }); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 46dbc66..b09f0bf 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -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 = () => {
- +