mirror of
https://github.com/PrimalHQ/primal-web-app.git
synced 2024-10-01 17:31:13 +00:00
Refactor notes
This commit is contained in:
parent
21457cc2c5
commit
ded15540a2
@ -234,13 +234,158 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.verificationFailed {
|
||||
display: inline-block;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.notePrimary {
|
||||
position: relative;
|
||||
background-color: var(--background-card);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 12px;
|
||||
padding-top: 0;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 2px;
|
||||
margin-top: 2px;
|
||||
cursor: text;
|
||||
|
||||
|
||||
.message {
|
||||
position: relative;
|
||||
grid-area: message;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
margin-bottom: 12px;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.messageFade {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 610px;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
background-image: var(--fade-note-vertical);
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.noteNotificationLink {
|
||||
text-decoration: none;
|
||||
color: unset;
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
// background: var(--brand-gradient-vertical);
|
||||
background-color: var(--background-card);
|
||||
border-radius: 6px;
|
||||
display: block;
|
||||
transition: 0.2s padding;
|
||||
margin-top: 6px;
|
||||
|
||||
.noteNotifications {
|
||||
background-color: var(--background-site);
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: 1fr;
|
||||
grid-template-areas: "content";
|
||||
padding: 0px;
|
||||
border-radius: 4px;
|
||||
transition: 0.2s border-radius ease-out;
|
||||
|
||||
.content {
|
||||
grid-area: content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
||||
.message {
|
||||
grid-area: message;
|
||||
color: var(--text-primary);
|
||||
word-break: break-word;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
width: 100%;
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// &:hover {
|
||||
// padding-left: 4px;
|
||||
// transition: 0.2s padding;
|
||||
// border-radius: 4px;
|
||||
// >div {
|
||||
// border-radius: 0px 4px 4px 0px;
|
||||
// transition: 0.2s border-radius ease-out;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
.context {
|
||||
background: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
|
||||
.contextButton {
|
||||
width: 42px;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 720px) {
|
||||
.note {
|
||||
width: 100dvw;
|
||||
@ -258,6 +403,24 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.notePrimary {
|
||||
width: 100vw;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
padding-right: 12px;
|
||||
.content {
|
||||
margin-left: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.noteNotificationLink {
|
||||
.noteNotifications {
|
||||
grid-template-columns: 1fr;
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
padding-right: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.upRightFloater {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { A } from '@solidjs/router';
|
||||
import { Component, Show } from 'solid-js';
|
||||
import { PrimalNote } from '../../types/primal';
|
||||
import { batch, Component, Match, Show, Switch } from 'solid-js';
|
||||
import { PrimalNote, ZapOption } from '../../types/primal';
|
||||
import ParsedNote from '../ParsedNote/ParsedNote';
|
||||
import NoteFooter from './NoteFooter/NoteFooter';
|
||||
|
||||
@ -12,71 +12,268 @@ import Avatar from '../Avatar/Avatar';
|
||||
import NoteAuthorInfo from './NoteAuthorInfo';
|
||||
import NoteRepostHeader from './NoteRepostHeader';
|
||||
import NoteReplyToHeader from './NoteReplyToHeader';
|
||||
import BookmarkNote from '../BookmarkNote/BookmarkNote';
|
||||
import NoteHeader from './NoteHeader/NoteHeader';
|
||||
import { createStore } from 'solid-js/store';
|
||||
import { CustomZapInfo, useAppContext } from '../../contexts/AppContext';
|
||||
import NoteContextTrigger from './NoteContextTrigger';
|
||||
|
||||
const Note: Component<{ note: PrimalNote, id?: string, parent?: boolean, shorten?: boolean }> = (props) => {
|
||||
export type NoteFooterState = {
|
||||
likes: number,
|
||||
liked: boolean,
|
||||
reposts: number,
|
||||
reposted: boolean,
|
||||
replies: number,
|
||||
replied: boolean,
|
||||
zapCount: number,
|
||||
satsZapped: number,
|
||||
zappedAmount: number,
|
||||
zapped: boolean,
|
||||
zappedNow: boolean,
|
||||
isZapping: boolean,
|
||||
showZapAnim: boolean,
|
||||
hideZapIcon: boolean,
|
||||
isRepostMenuVisible: boolean,
|
||||
};
|
||||
|
||||
const Note: Component<{
|
||||
note: PrimalNote,
|
||||
id?: string,
|
||||
parent?: boolean,
|
||||
shorten?: boolean,
|
||||
noteType?: 'feed' | 'primary' | 'notification'
|
||||
}> = (props) => {
|
||||
|
||||
const threadContext = useThreadContext();
|
||||
const app = useAppContext();
|
||||
const intl = useIntl();
|
||||
|
||||
const noteType = () => props.noteType || 'feed';
|
||||
|
||||
const repost = () => props.note.repost;
|
||||
|
||||
const navToThread = (note: PrimalNote) => {
|
||||
threadContext?.actions.setPrimaryNote(note);
|
||||
};
|
||||
|
||||
return (
|
||||
<A
|
||||
id={props.id}
|
||||
class={`${styles.note} ${props.parent ? styles.parent : ''}`}
|
||||
href={`/e/${props.note?.post.noteId}`}
|
||||
onClick={() => navToThread(props.note)}
|
||||
data-event={props.note.post.id}
|
||||
data-event-bech32={props.note.post.noteId}
|
||||
draggable={false}
|
||||
>
|
||||
<div class={styles.header}>
|
||||
<Show when={repost()}>
|
||||
<NoteRepostHeader note={props.note} />
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.content}>
|
||||
<div class={styles.leftSide}>
|
||||
<A href={`/p/${props.note.user.npub}`}>
|
||||
<Avatar user={props.note.user} size="vs" />
|
||||
</A>
|
||||
<Show
|
||||
when={props.parent}
|
||||
>
|
||||
<div class={styles.ancestorLine}></div>
|
||||
</Show>
|
||||
</div>
|
||||
const [footerState, updateFooterState] = createStore({
|
||||
likes: props.note.post.likes,
|
||||
liked: props.note.post.noteActions.liked,
|
||||
reposts: props.note.post.reposts,
|
||||
reposted: props.note.post.noteActions.reposted,
|
||||
replies: props.note.post.replies,
|
||||
replied: props.note.post.noteActions.replied,
|
||||
zapCount: props.note.post.zaps,
|
||||
satsZapped: props.note.post.satszapped,
|
||||
zapped: props.note.post.noteActions.zapped,
|
||||
zappedAmount: 0,
|
||||
zappedNow: false,
|
||||
isZapping: false,
|
||||
showZapAnim: false,
|
||||
hideZapIcon: false,
|
||||
isRepostMenuVisible: false,
|
||||
});
|
||||
|
||||
<div class={styles.rightSide}>
|
||||
<NoteAuthorInfo
|
||||
author={props.note.user}
|
||||
time={props.note.post.created_at}
|
||||
/>
|
||||
let noteContextMenu: HTMLDivElement | undefined;
|
||||
|
||||
const onConfirmZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
batch(() => {
|
||||
updateFooterState('zappedAmount', () => zapOption.amount || 0);
|
||||
updateFooterState('zappedNow', () => true);
|
||||
updateFooterState('zapped', () => true);
|
||||
updateFooterState('showZapAnim', () => true)
|
||||
});
|
||||
};
|
||||
|
||||
const onSuccessZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
batch(() => {
|
||||
updateFooterState('isZapping', () => false);
|
||||
updateFooterState('zappedNow', () => false);
|
||||
updateFooterState('showZapAnim', () => false);
|
||||
updateFooterState('hideZapIcon', () => false);
|
||||
updateFooterState('zapped', () => true);
|
||||
});
|
||||
};
|
||||
|
||||
const onFailZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
batch(() => {
|
||||
updateFooterState('zappedAmount', () => -(zapOption.amount || 0));
|
||||
updateFooterState('isZapping', () => false);
|
||||
updateFooterState('zappedNow', () => true);
|
||||
updateFooterState('showZapAnim', () => false);
|
||||
updateFooterState('hideZapIcon', () => false);
|
||||
updateFooterState('zapped', () => props.note.post.noteActions.zapped);
|
||||
});
|
||||
};
|
||||
|
||||
const onCancelZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
batch(() => {
|
||||
updateFooterState('zappedAmount', () => -(zapOption.amount || 0));
|
||||
updateFooterState('isZapping', () => false);
|
||||
updateFooterState('zappedNow', () => true);
|
||||
updateFooterState('showZapAnim', () => false);
|
||||
updateFooterState('hideZapIcon', () => false);
|
||||
updateFooterState('zapped', () => props.note.post.noteActions.zapped);
|
||||
});
|
||||
};
|
||||
|
||||
const customZapInfo: CustomZapInfo = {
|
||||
note: props.note,
|
||||
onConfirm: onConfirmZap,
|
||||
onSuccess: onSuccessZap,
|
||||
onFail: onFailZap,
|
||||
onCancel: onCancelZap,
|
||||
};
|
||||
|
||||
const onContextMenuTrigger = () => {
|
||||
app?.actions.openContextMenu(
|
||||
props.note,
|
||||
noteContextMenu?.getBoundingClientRect(),
|
||||
() => {
|
||||
app?.actions.openCustomZapModal(customZapInfo);
|
||||
},
|
||||
() => {
|
||||
app?.actions.openReactionModal(props.note.post.id, {
|
||||
likes: footerState.likes,
|
||||
zaps: footerState.zapCount,
|
||||
reposts: footerState.reposts,
|
||||
quotes: 0,
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={noteType() === 'notification'}>
|
||||
<A
|
||||
id={props.id}
|
||||
class={styles.noteNotificationLink}
|
||||
href={`/e/${props.note?.post.noteId}`}
|
||||
onClick={() => navToThread(props.note)}
|
||||
data-event={props.note.post.id}
|
||||
data-event-bech32={props.note.post.noteId}
|
||||
>
|
||||
<div class={styles.noteNotifications}>
|
||||
<div class={styles.content}>
|
||||
<div class={styles.message}>
|
||||
<ParsedNote note={props.note} shorten={true} />
|
||||
</div>
|
||||
|
||||
<div class={styles.footer}>
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={footerState}
|
||||
updateState={updateFooterState}
|
||||
customZapInfo={customZapInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
</Match>
|
||||
|
||||
<Match when={noteType() === 'primary'}>
|
||||
<div
|
||||
id={props.id}
|
||||
class={styles.notePrimary}
|
||||
data-event={props.note.post.id}
|
||||
data-event-bech32={props.note.post.noteId}
|
||||
>
|
||||
<div class={styles.border}></div>
|
||||
|
||||
<NoteHeader note={props.note} primary={true} />
|
||||
|
||||
<div class={styles.upRightFloater}>
|
||||
<BookmarkNote note={props.note} />
|
||||
</div>
|
||||
|
||||
<NoteReplyToHeader note={props.note} />
|
||||
|
||||
<div class={styles.message}>
|
||||
<ParsedNote
|
||||
note={props.note}
|
||||
shorten={props.shorten}
|
||||
width={Math.min(528, window.innerWidth - 72)}
|
||||
<NoteContextTrigger
|
||||
ref={noteContextMenu}
|
||||
onClick={onContextMenuTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NoteFooter note={props.note} />
|
||||
<div class={styles.content}>
|
||||
|
||||
<div class={styles.message}>
|
||||
<ParsedNote note={props.note} width={Math.min(574, window.innerWidth)} />
|
||||
</div>
|
||||
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={footerState}
|
||||
updateState={updateFooterState}
|
||||
customZapInfo={customZapInfo}
|
||||
wide={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
)
|
||||
</Match>
|
||||
|
||||
<Match when={noteType() === 'feed'}>
|
||||
|
||||
<A
|
||||
id={props.id}
|
||||
class={`${styles.note} ${props.parent ? styles.parent : ''}`}
|
||||
href={`/e/${props.note?.post.noteId}`}
|
||||
onClick={() => navToThread(props.note)}
|
||||
data-event={props.note.post.id}
|
||||
data-event-bech32={props.note.post.noteId}
|
||||
draggable={false}
|
||||
>
|
||||
<div class={styles.header}>
|
||||
<Show when={repost()}>
|
||||
<NoteRepostHeader note={props.note} />
|
||||
</Show>
|
||||
</div>
|
||||
<div class={styles.content}>
|
||||
<div class={styles.leftSide}>
|
||||
<A href={`/p/${props.note.user.npub}`}>
|
||||
<Avatar user={props.note.user} size="vs" />
|
||||
</A>
|
||||
<Show
|
||||
when={props.parent}
|
||||
>
|
||||
<div class={styles.ancestorLine}></div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class={styles.rightSide}>
|
||||
<NoteAuthorInfo
|
||||
author={props.note.user}
|
||||
time={props.note.post.created_at}
|
||||
/>
|
||||
|
||||
<div class={styles.upRightFloater}>
|
||||
<NoteContextTrigger
|
||||
ref={noteContextMenu}
|
||||
onClick={onContextMenuTrigger}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NoteReplyToHeader note={props.note} />
|
||||
|
||||
<div class={styles.message}>
|
||||
<ParsedNote
|
||||
note={props.note}
|
||||
shorten={props.shorten}
|
||||
width={Math.min(528, window.innerWidth - 72)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<NoteFooter
|
||||
note={props.note}
|
||||
state={footerState}
|
||||
updateState={updateFooterState}
|
||||
customZapInfo={customZapInfo}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</A>
|
||||
</Match>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
|
||||
export default hookForDev(Note);
|
||||
|
@ -51,6 +51,7 @@ const NoteContextMenu: Component<{
|
||||
|
||||
if (!props.open) {
|
||||
context.setAttribute('style',`top: -1024px; left: -1034px;`);
|
||||
return;
|
||||
}
|
||||
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
|
28
src/components/Note/NoteContextTrigger.tsx
Normal file
28
src/components/Note/NoteContextTrigger.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { Component } from 'solid-js';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
|
||||
import styles from './Note.module.scss';
|
||||
|
||||
|
||||
const NoteContextTrigger: Component<{
|
||||
ref: HTMLDivElement | undefined,
|
||||
id?: string,
|
||||
onClick?: () => void,
|
||||
}> = (props) => {
|
||||
return (
|
||||
<div ref={props.ref} class={styles.context}>
|
||||
<button
|
||||
class={styles.contextButton}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
props.onClick && props.onClick();
|
||||
}}
|
||||
>
|
||||
<div class={styles.contextIcon} ></div>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default hookForDev(NoteContextTrigger);
|
@ -43,12 +43,12 @@
|
||||
|
||||
.footer {
|
||||
display: grid;
|
||||
grid-template-columns: 128px 128px 128px 128px 16px;
|
||||
grid-template-columns: 125px 125px 125px 125px 28px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
&.wide {
|
||||
grid-template-columns: 140px 140px 140px 138px 16px;
|
||||
grid-template-columns: 137px 137px 137px 135px 28px;
|
||||
}
|
||||
|
||||
.context {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Component, createEffect, createSignal, Show } from 'solid-js';
|
||||
import { MenuItem, PrimalNote, ZapOption } from '../../../types/primal';
|
||||
import { batch, Component, createEffect, Show } from 'solid-js';
|
||||
import { MenuItem, PrimalNote } from '../../../types/primal';
|
||||
import { sendRepost } from '../../../lib/notes';
|
||||
|
||||
import styles from './NoteFooter.module.scss';
|
||||
@ -9,7 +9,6 @@ import { useIntl } from '@cookbook/solid-intl';
|
||||
|
||||
import { truncateNumber } from '../../../lib/notifications';
|
||||
import { canUserReceiveZaps, zapNote } from '../../../lib/zap';
|
||||
import CustomZap from '../../CustomZap/CustomZap';
|
||||
import { useSettingsContext } from '../../../contexts/SettingsContext';
|
||||
|
||||
import zapMD from '../../../assets/lottie/zap_md.json';
|
||||
@ -19,8 +18,19 @@ import { hookForDev } from '../../../lib/devTools';
|
||||
import { getScreenCordinates } from '../../../utils';
|
||||
import ZapAnimation from '../../ZapAnimation/ZapAnimation';
|
||||
import { CustomZapInfo, useAppContext } from '../../../contexts/AppContext';
|
||||
import NoteFooterActionButton from './NoteFooterActionButton';
|
||||
import { NoteFooterState } from '../Note';
|
||||
import { SetStoreFunction } from 'solid-js/store';
|
||||
import BookmarkNote from '../../BookmarkNote/BookmarkNote';
|
||||
|
||||
const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> = (props) => {
|
||||
const NoteFooter: Component<{
|
||||
note: PrimalNote,
|
||||
wide?: boolean,
|
||||
id?: string,
|
||||
state: NoteFooterState,
|
||||
updateState: SetStoreFunction<NoteFooterState>,
|
||||
customZapInfo: CustomZapInfo,
|
||||
}> = (props) => {
|
||||
|
||||
const account = useAccountContext();
|
||||
const toast = useToastContext();
|
||||
@ -30,28 +40,8 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
|
||||
let medZapAnimation: HTMLElement | undefined;
|
||||
|
||||
const [liked, setLiked] = createSignal(props.note.post.noteActions.liked);
|
||||
const [zapped, setZapped] = createSignal(props.note.post.noteActions.zapped);
|
||||
const [replied, setReplied] = createSignal(props.note.post.noteActions.replied);
|
||||
const [reposted, setReposted] = createSignal(props.note.post.noteActions.reposted);
|
||||
|
||||
const [likes, setLikes] = createSignal(props.note.post.likes);
|
||||
const [reposts, setReposts] = createSignal(props.note.post.reposts);
|
||||
const [replies, setReplies] = createSignal(props.note.post.replies);
|
||||
const [zapCount, setZapCount] = createSignal(props.note.post.zaps);
|
||||
const [zaps, setZaps] = createSignal(props.note.post.satszapped);
|
||||
|
||||
const [isRepostMenuVisible, setIsRepostMenuVisible] = createSignal(false);
|
||||
|
||||
const [showZapAnim, setShowZapAnim] = createSignal(false);
|
||||
const [hideZapIcon, setHideZapIcon] = createSignal(false);
|
||||
const [zappedNow, setZappedNow] = createSignal(false);
|
||||
const [zappedAmount, setZappedAmount] = createSignal(0);
|
||||
const [isZapping, setIsZapping] = createSignal(false);
|
||||
|
||||
let quickZapDelay = 0;
|
||||
let footerDiv: HTMLDivElement | undefined;
|
||||
let noteContextMenu: HTMLDivElement | undefined;
|
||||
let repostMenu: HTMLDivElement | undefined;
|
||||
|
||||
const repostMenuItems: MenuItem[] = [
|
||||
@ -67,17 +57,16 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const onClickOutside = (e: MouseEvent) => {
|
||||
if (
|
||||
!document?.getElementById(`repost_menu_${props.note.post.id}`)?.contains(e.target as Node)
|
||||
) {
|
||||
setIsRepostMenuVisible(false);
|
||||
props.updateState('isRepostMenuVisible', () => false);
|
||||
}
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
if (isRepostMenuVisible()) {
|
||||
if (props.state.isRepostMenuVisible) {
|
||||
document.addEventListener('click', onClickOutside);
|
||||
}
|
||||
else {
|
||||
@ -87,7 +76,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
|
||||
const showRepostMenu = (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
setIsRepostMenuVisible(true);
|
||||
props.updateState('isRepostMenuVisible', () => true);
|
||||
};
|
||||
|
||||
const doQuote = () => {
|
||||
@ -95,7 +84,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
account?.actions.showGetStarted();
|
||||
return;
|
||||
}
|
||||
setIsRepostMenuVisible(false);
|
||||
props.updateState('isRepostMenuVisible', () => false);
|
||||
account?.actions?.quoteNote(`nostr:${props.note.post.noteId}`);
|
||||
account?.actions?.showNewNoteForm();
|
||||
};
|
||||
@ -117,13 +106,16 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRepostMenuVisible(false);
|
||||
props.updateState('isRepostMenuVisible', () => false);
|
||||
|
||||
const { success } = await sendRepost(props.note, account.relays, account.relaySettings);
|
||||
|
||||
if (success) {
|
||||
setReposts(reposts() + 1);
|
||||
setReposted(true);
|
||||
batch(() => {
|
||||
props.updateState('reposts', (r) => r + 1);
|
||||
props.updateState('reposted', () => true);
|
||||
});
|
||||
|
||||
toast?.sendSuccess(
|
||||
intl.formatMessage(t.repostSuccess),
|
||||
);
|
||||
@ -160,63 +152,20 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
const success = await account.actions.addLike(props.note);
|
||||
|
||||
if (success) {
|
||||
setLikes(likes() + 1);
|
||||
setLiked(true);
|
||||
batch(() => {
|
||||
props.updateState('likes', (l) => l + 1);
|
||||
props.updateState('liked', () => true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onConfirmZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
setZappedAmount(() => zapOption.amount || 0);
|
||||
setZappedNow(true);
|
||||
setZapped(true);
|
||||
animateZap();
|
||||
};
|
||||
|
||||
const onSuccessZap = (zapOption: ZapOption) => {
|
||||
app?.actions.closeCustomZapModal();
|
||||
setIsZapping(false);
|
||||
setZappedNow(false);
|
||||
setShowZapAnim(false);
|
||||
setHideZapIcon(false);
|
||||
setZapped(true);
|
||||
};
|
||||
|
||||
const onFailZap = (zapOption: ZapOption) => {
|
||||
setZappedAmount(() => -(zapOption.amount || 0));
|
||||
setZappedNow(true);
|
||||
app?.actions.closeCustomZapModal();
|
||||
setIsZapping(false);
|
||||
setShowZapAnim(false);
|
||||
setHideZapIcon(false);
|
||||
setZapped(props.note.post.noteActions.zapped);
|
||||
};
|
||||
|
||||
const onCancelZap = (zapOption: ZapOption) => {
|
||||
setZappedAmount(() => -(zapOption.amount || 0));
|
||||
setZappedNow(true);
|
||||
app?.actions.closeCustomZapModal();
|
||||
setIsZapping(false);
|
||||
setShowZapAnim(false);
|
||||
setHideZapIcon(false);
|
||||
setZapped(props.note.post.noteActions.zapped);
|
||||
};
|
||||
|
||||
const customZapInfo: CustomZapInfo = {
|
||||
note: props.note,
|
||||
onConfirm: onConfirmZap,
|
||||
onSuccess: onSuccessZap,
|
||||
onFail: onFailZap,
|
||||
onCancel: onCancelZap,
|
||||
};
|
||||
|
||||
const startZap = (e: MouseEvent | TouchEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!account?.hasPublicKey()) {
|
||||
account?.actions.showGetStarted()
|
||||
setIsZapping(false);
|
||||
account?.actions.showGetStarted();
|
||||
props.updateState('isZapping', () => false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -231,13 +180,13 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
toast?.sendWarning(
|
||||
intl.formatMessage(t.zapUnavailable),
|
||||
);
|
||||
setIsZapping(false);
|
||||
props.updateState('isZapping', () => false);
|
||||
return;
|
||||
}
|
||||
|
||||
quickZapDelay = setTimeout(() => {
|
||||
app?.actions.openCustomZapModal(customZapInfo);
|
||||
setIsZapping(true);
|
||||
app?.actions.openCustomZapModal(props.customZapInfo);
|
||||
props.updateState('isZapping', () => true);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
@ -262,9 +211,8 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
};
|
||||
|
||||
const animateZap = () => {
|
||||
setShowZapAnim(true);
|
||||
setTimeout(() => {
|
||||
setHideZapIcon(true);
|
||||
props.updateState('hideZapIcon', () => true);
|
||||
|
||||
if (!medZapAnimation) {
|
||||
return;
|
||||
@ -277,8 +225,10 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
medZapAnimation.style.top = `${newTop}px`;
|
||||
|
||||
const onAnimDone = () => {
|
||||
setShowZapAnim(false);
|
||||
setHideZapIcon(false);
|
||||
batch(() => {
|
||||
props.updateState('showZapAnim', () => false);
|
||||
props.updateState('hideZapIcon', () => false);
|
||||
});
|
||||
medZapAnimation?.removeEventListener('complete', onAnimDone);
|
||||
}
|
||||
|
||||
@ -302,19 +252,24 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
return;
|
||||
}
|
||||
|
||||
setZappedAmount(() => settings?.defaultZap.amount || 0);
|
||||
setZappedNow(true);
|
||||
animateZap();
|
||||
batch(() => {
|
||||
props.updateState('zappedAmount', () => settings?.defaultZap.amount || 0);
|
||||
props.updateState('zappedNow', () => true);
|
||||
props.updateState('showZapAnim', () => true);
|
||||
});
|
||||
const success = await zapNote(props.note, account.publicKey, settings?.defaultZap.amount || 10, settings?.defaultZap.message || '', account.relays);
|
||||
setIsZapping(false);
|
||||
|
||||
props.updateState('isZapping', () => false);
|
||||
|
||||
if (success) {
|
||||
return;
|
||||
}
|
||||
|
||||
setZappedAmount(() => -(settings?.defaultZap.amount || 0));
|
||||
setZappedNow(true);
|
||||
setZapped(props.note.post.noteActions.zapped);
|
||||
batch(() => {
|
||||
props.updateState('zappedAmount', () => -(settings?.defaultZap.amount || 0));
|
||||
props.updateState('zappedNow', () => true);
|
||||
props.updateState('zapped', () => props.note.post.noteActions.zapped);
|
||||
});
|
||||
}
|
||||
|
||||
const buttonTypeClasses: Record<string, string> = {
|
||||
@ -324,51 +279,21 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
repost: styles.repostType,
|
||||
};
|
||||
|
||||
const actionButton = (opts: {
|
||||
type: 'zap' | 'like' | 'reply' | 'repost',
|
||||
disabled?: boolean,
|
||||
highlighted?: boolean,
|
||||
onClick?: (e: MouseEvent) => void,
|
||||
onMouseDown?: (e: MouseEvent) => void,
|
||||
onMouseUp?: (e: MouseEvent) => void,
|
||||
onTouchStart?: (e: TouchEvent) => void,
|
||||
onTouchEnd?: (e: TouchEvent) => void,
|
||||
label: string | number,
|
||||
hidden?: boolean,
|
||||
title?: string,
|
||||
}) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
id={`btn_${opts.type}_${props.note.post.id}`}
|
||||
class={`${styles.stat} ${opts.highlighted ? styles.highlighted : ''}`}
|
||||
onClick={opts.onClick ?? (() => {})}
|
||||
onMouseDown={opts.onMouseDown ?? (() => {})}
|
||||
onMouseUp={opts.onMouseUp ?? (() => {})}
|
||||
onTouchStart={opts.onTouchStart ?? (() => {})}
|
||||
onTouchEnd={opts.onTouchEnd ?? (() => {})}
|
||||
disabled={opts.disabled}
|
||||
>
|
||||
<div class={`${buttonTypeClasses[opts.type]}`}>
|
||||
<div
|
||||
class={styles.icon}
|
||||
style={opts.hidden ? 'visibility: hidden': 'visibility: visible'}
|
||||
></div>
|
||||
<div class={styles.statNumber}>{opts.label || ''}</div>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
createEffect(() => {
|
||||
if (props.state.zappedNow) {
|
||||
batch(() => {
|
||||
props.updateState('zapCount', (z) => z + 1);
|
||||
props.updateState('satsZapped', (z) => z + props.state.zappedAmount);
|
||||
props.updateState('zapped', () => true);
|
||||
});
|
||||
props.updateState('zappedNow', () => false);
|
||||
}
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
|
||||
if (zappedNow()) {
|
||||
setZapCount(c => c + 1);
|
||||
setZaps((z) => z + zappedAmount());
|
||||
setZapped(true);
|
||||
setZappedNow(false);
|
||||
if (props.state.showZapAnim) {
|
||||
animateZap();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
const determineOrient = () => {
|
||||
@ -380,7 +305,7 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
return (
|
||||
<div id={props.id} class={`${styles.footer} ${props.wide ? styles.wide : ''}`} ref={footerDiv} onClick={(e) => {e.preventDefault();}}>
|
||||
|
||||
<Show when={showZapAnim()}>
|
||||
<Show when={props.state.showZapAnim}>
|
||||
<ZapAnimation
|
||||
id={`note-med-zap-${props.note.post.id}`}
|
||||
src={zapMD}
|
||||
@ -389,41 +314,43 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
/>
|
||||
</Show>
|
||||
|
||||
{actionButton({
|
||||
onClick: doReply,
|
||||
type: 'reply',
|
||||
highlighted: replied(),
|
||||
label: replies() === 0 ? '' : truncateNumber(replies(), 2),
|
||||
title: replies().toLocaleString(),
|
||||
})}
|
||||
<NoteFooterActionButton
|
||||
note={props.note}
|
||||
onClick={doReply}
|
||||
type="reply"
|
||||
highlighted={props.state.replied}
|
||||
label={props.state.replies === 0 ? '' : truncateNumber(props.state.replies, 2)}
|
||||
title={props.state.replies.toLocaleString()}
|
||||
/>
|
||||
|
||||
{actionButton({
|
||||
onClick: (e: MouseEvent) => e.preventDefault(),
|
||||
onMouseDown: startZap,
|
||||
onMouseUp: commitZap,
|
||||
onTouchStart: startZap,
|
||||
onTouchEnd: commitZap,
|
||||
type: 'zap',
|
||||
highlighted: zapped() || isZapping(),
|
||||
label: zaps() === 0 ? '' : truncateNumber(zaps(), 2),
|
||||
hidden: hideZapIcon(),
|
||||
title: zaps().toLocaleString(),
|
||||
})}
|
||||
|
||||
{actionButton({
|
||||
onClick: doLike,
|
||||
type: 'like',
|
||||
highlighted: liked(),
|
||||
label: likes() === 0 ? '' : truncateNumber(likes(), 2),
|
||||
title: likes().toLocaleString(),
|
||||
})}
|
||||
<NoteFooterActionButton
|
||||
note={props.note}
|
||||
onClick={(e: MouseEvent) => e.preventDefault()}
|
||||
onMouseDown={startZap}
|
||||
onMouseUp={commitZap}
|
||||
onTouchStart={startZap}
|
||||
onTouchEnd={commitZap}
|
||||
type="zap"
|
||||
highlighted={props.state.zapped || props.state.isZapping}
|
||||
label={props.state.satsZapped === 0 ? '' : truncateNumber(props.state.satsZapped, 2)}
|
||||
hidden={props.state.hideZapIcon}
|
||||
title={props.state.satsZapped.toLocaleString()}
|
||||
/>
|
||||
|
||||
<NoteFooterActionButton
|
||||
note={props.note}
|
||||
onClick={doLike}
|
||||
type="like"
|
||||
highlighted={props.state.liked}
|
||||
label={props.state.likes === 0 ? '' : truncateNumber(props.state.likes, 2)}
|
||||
title={props.state.likes.toLocaleString()}
|
||||
/>
|
||||
|
||||
<button
|
||||
id={`btn_repost_${props.note.post.id}`}
|
||||
class={`${styles.stat} ${reposted() ? styles.highlighted : ''}`}
|
||||
class={`${styles.stat} ${props.state.reposted ? styles.highlighted : ''}`}
|
||||
onClick={showRepostMenu}
|
||||
title={reposts().toLocaleString()}
|
||||
title={props.state.reposts.toLocaleString()}
|
||||
>
|
||||
<div
|
||||
class={`${buttonTypeClasses.repost}`}
|
||||
@ -434,45 +361,20 @@ const NoteFooter: Component<{ note: PrimalNote, wide?: boolean, id?: string }> =
|
||||
style={'visibility: visible'}
|
||||
></div>
|
||||
<div class={styles.statNumber}>
|
||||
{reposts() === 0 ? '' : truncateNumber(reposts(), 2)}
|
||||
{props.state.reposts === 0 ? '' : truncateNumber(props.state.reposts, 2)}
|
||||
</div>
|
||||
<PrimalMenu
|
||||
id={`repost_menu_${props.note.post.id}`}
|
||||
items={repostMenuItems}
|
||||
position="note_footer"
|
||||
orientation={determineOrient()}
|
||||
hidden={!isRepostMenuVisible()}
|
||||
hidden={!props.state.isRepostMenuVisible}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div ref={noteContextMenu} class={styles.context}>
|
||||
<button
|
||||
class={styles.contextButton}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
<BookmarkNote note={props.note} />
|
||||
|
||||
app?.actions.openContextMenu(
|
||||
props.note,
|
||||
noteContextMenu?.getBoundingClientRect(),
|
||||
() => {
|
||||
app?.actions.openCustomZapModal(customZapInfo);
|
||||
},
|
||||
() => {
|
||||
app?.actions.openReactionModal(props.note.post.id, {
|
||||
likes: likes(),
|
||||
zaps: zapCount(),
|
||||
reposts: reposts(),
|
||||
quotes: 0,
|
||||
});
|
||||
}
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div class={styles.contextIcon} ></div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
50
src/components/Note/NoteFooter/NoteFooterActionButton.tsx
Normal file
50
src/components/Note/NoteFooter/NoteFooterActionButton.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Component, createEffect, onCleanup } from 'solid-js';
|
||||
import { PrimalNote } from '../../../types/primal';
|
||||
|
||||
import styles from './NoteFooter.module.scss';
|
||||
|
||||
const buttonTypeClasses: Record<string, string> = {
|
||||
zap: styles.zapType,
|
||||
like: styles.likeType,
|
||||
reply: styles.replyType,
|
||||
repost: styles.repostType,
|
||||
};
|
||||
|
||||
const NoteFooterActionButton: Component<{
|
||||
type: 'zap' | 'like' | 'reply' | 'repost',
|
||||
note: PrimalNote,
|
||||
disabled?: boolean,
|
||||
highlighted?: boolean,
|
||||
onClick?: (e: MouseEvent) => void,
|
||||
onMouseDown?: (e: MouseEvent) => void,
|
||||
onMouseUp?: (e: MouseEvent) => void,
|
||||
onTouchStart?: (e: TouchEvent) => void,
|
||||
onTouchEnd?: (e: TouchEvent) => void,
|
||||
label: string | number,
|
||||
hidden?: boolean,
|
||||
title?: string,
|
||||
}> = (props) => {
|
||||
|
||||
return (
|
||||
<button
|
||||
id={`btn_${props.type}_${props.note.post.id}`}
|
||||
class={`${styles.stat} ${props.highlighted ? styles.highlighted : ''}`}
|
||||
onClick={props.onClick ?? (() => {})}
|
||||
onMouseDown={props.onMouseDown ?? (() => {})}
|
||||
onMouseUp={props.onMouseUp ?? (() => {})}
|
||||
onTouchStart={props.onTouchStart ?? (() => {})}
|
||||
onTouchEnd={props.onTouchEnd ?? (() => {})}
|
||||
disabled={props.disabled}
|
||||
>
|
||||
<div class={`${buttonTypeClasses[props.type]}`}>
|
||||
<div
|
||||
class={styles.icon}
|
||||
style={props.hidden ? 'visibility: hidden': 'visibility: visible'}
|
||||
></div>
|
||||
<div class={styles.statNumber}>{props.label || ''}</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default NoteFooterActionButton;
|
@ -5,7 +5,7 @@
|
||||
|
||||
padding-top: 12px;
|
||||
padding-bottom: 17px;
|
||||
border-bottom: 1px solid var(--subtile-devider);
|
||||
border-bottom: 1px solid var(--devider);
|
||||
|
||||
.newBubble {
|
||||
position: absolute;
|
||||
|
@ -33,6 +33,7 @@ import NotificationNote from '../Note/NotificationNote/NotificationNote';
|
||||
import NotificationAvatar from '../NotificationAvatar/NotificationAvatar';
|
||||
import { notificationsNew as t } from '../../translations';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import Note from '../Note/Note';
|
||||
|
||||
const typeIcons: Record<string, string> = {
|
||||
[NotificationType.NEW_USER_FOLLOWED_YOU]: userFollow,
|
||||
@ -183,9 +184,10 @@ const NotificationItem: Component<NotificationItemProps> = (props) => {
|
||||
>
|
||||
<div class={styles.reference}>
|
||||
<Show when={props.note}>
|
||||
<NotificationNote
|
||||
<Note
|
||||
// @ts-ignore
|
||||
note={props.note}
|
||||
noteType="notification"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -33,6 +33,7 @@ import NotificationNote from '../Note/NotificationNote/NotificationNote';
|
||||
import { truncateNumber } from '../../lib/notifications';
|
||||
import { notificationsOld as t } from '../../translations';
|
||||
import { hookForDev } from '../../lib/devTools';
|
||||
import Note from '../Note/Note';
|
||||
|
||||
const typeIcons: Record<string, string> = {
|
||||
[NotificationType.NEW_USER_FOLLOWED_YOU]: userFollow,
|
||||
@ -148,9 +149,10 @@ const NotificationItemOld: Component<NotificationItemProps> = (props) => {
|
||||
>
|
||||
<div class={styles.reference}>
|
||||
<Show when={note()}>
|
||||
<NotificationNote
|
||||
<Note
|
||||
// @ts-ignore
|
||||
note={note()}
|
||||
noteType="notification"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -72,7 +72,14 @@ export const getUserFeed = (user_pubkey: string | undefined, pubkey: string | un
|
||||
return;
|
||||
}
|
||||
|
||||
let payload = { pubkey, limit, notes } ;
|
||||
let payload: {
|
||||
pubkey: string,
|
||||
limit: number,
|
||||
notes: 'authored' | 'replies' | 'bookmarks',
|
||||
user_pubkey?: string,
|
||||
until?: number,
|
||||
offset?: number,
|
||||
} = { pubkey, limit, notes } ;
|
||||
|
||||
if (user_pubkey) {
|
||||
payload.user_pubkey = user_pubkey;
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
.oldNotifications {
|
||||
position: relative;
|
||||
padding-right: 2px;
|
||||
// padding-right: 2px;
|
||||
}
|
||||
|
||||
.loader {
|
||||
@ -62,28 +62,6 @@
|
||||
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
.newContentNotification {
|
||||
left: calc(calc(100vw - 1032px) / 2 + 48px + 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1087px) {
|
||||
.newContentNotification {
|
||||
left: calc(calc(100vw - 720px) / 2 + 48px + 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 720px) {
|
||||
.newContentNotification {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
.notificationTabs {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@ -122,7 +100,7 @@
|
||||
}
|
||||
|
||||
.notificationTabContent {
|
||||
width: 100%;
|
||||
width: 602px;
|
||||
}
|
||||
|
||||
.notificationTabIndicator {
|
||||
@ -148,3 +126,27 @@
|
||||
border-radius: 0 0 8px 8px;
|
||||
padding-top: 22px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1300px) {
|
||||
.newContentNotification {
|
||||
left: calc(calc(100vw - 1032px) / 2 + 48px + 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 1087px) {
|
||||
.newContentNotification {
|
||||
left: calc(calc(100vw - 720px) / 2 + 48px + 32px);
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 720px) {
|
||||
.newContentNotification {
|
||||
left: 0;
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.notificationTabContent {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
@ -195,8 +195,9 @@ const Thread: Component = () => {
|
||||
</div>
|
||||
}>
|
||||
<div id="primary_note">
|
||||
<NotePrimary
|
||||
<Note
|
||||
note={primaryNote() as PrimalNote}
|
||||
noteType="primary"
|
||||
/>
|
||||
<Show when={account?.hasPublicKey()}>
|
||||
<ReplyToNote
|
||||
|
Loading…
Reference in New Issue
Block a user