Compare commits

...

7 Commits

Author SHA1 Message Date
Bojan Mojsilovic
16c7c1c650 fix some colors 2024-06-19 16:49:25 +02:00
Bojan Mojsilovic
236909abb2 Large Article layout 2024-06-19 16:40:03 +02:00
Bojan Mojsilovic
07b071bd61 Minor article layout tweaks 2024-06-19 15:01:09 +02:00
Bojan Mojsilovic
d798c2cc70 Fix article bookmarks 2024-06-19 14:37:09 +02:00
Bojan Mojsilovic
09a0b0cc85 Add zap Articles 2024-06-19 10:53:07 +02:00
Bojan Mojsilovic
3dc1a9ddda Better sidebar height 2024-06-19 10:31:18 +02:00
Bojan Mojsilovic
4df8b8f57f Fix stats 2024-06-19 10:13:16 +02:00
19 changed files with 221 additions and 284 deletions

View File

@ -1,7 +1,7 @@
import { useIntl } from '@cookbook/solid-intl';
import { Component, createEffect, createSignal, Match, Show, Switch } from 'solid-js';
import { APP_ID } from '../../App';
import { Kind } from '../../constants';
import { Kind, supportedBookmarkTypes } from '../../constants';
import { useAccountContext } from '../../contexts/AccountContext';
import { useAppContext } from '../../contexts/AppContext';
import { getUserFeed } from '../../lib/feed';
@ -24,12 +24,12 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
const [isBookmarked, setIsBookmarked] = createSignal(false);
const [bookmarkInProgress, setBookmarkInProgress] = createSignal(false);
createEffect(() => {
const note = props.note;
if (note) {
setIsBookmarked(() => account?.bookmarks.includes(note.id) || false);
const coor = `${note.msg.kind}:${note.pubkey}:${(note.msg.tags.find(t => t[0] === 'd') || [])[1]}`;
setIsBookmarked(() => account?.bookmarks.includes(coor) || false);
}
})
@ -37,7 +37,7 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
if (!account) return;
const bookmarks = bookmarkTags.reduce((acc, t) =>
t[0] === 'e' ? [...acc, t[1]] : [...acc]
supportedBookmarkTypes.includes(t[0]) ? [...acc, t[1]] : [...acc]
, []);
const date = Math.floor((new Date()).getTime() / 1000);
@ -52,8 +52,12 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
};
const addBookmark = async (bookmarkTags: string[][]) => {
if (account && props.note && !bookmarkTags.find(b => b[0] === 'e' && b[1] === props.note?.id)) {
const bookmarksToAdd = [...bookmarkTags, ['e', props.note.id]];
if (!account || !props.note) return;
const aTag = ['a', `${props.note.msg.kind}:${props.note.pubkey}:${(props.note.msg.tags.find(t => t[0] === 'd') || [])[1]}`];
if (!bookmarkTags.find(b => b[0] === aTag[0] && b[1] === aTag[1])) {
const bookmarksToAdd = [...bookmarkTags, [ ...aTag ]];
if (bookmarksToAdd.length < 2) {
logWarning('BOOKMARK ISSUE: ', `before_bookmark_${APP_ID}`);
@ -78,8 +82,12 @@ const BookmarkArticle: Component<{ note: PrimalArticle | undefined, large?: bool
}
const removeBookmark = async (bookmarks: string[][]) => {
if (account && bookmarks.find(b => b[0] === 'e' && b[1] === props.note?.id)) {
const bookmarksToAdd = bookmarks.filter(b => b[0] !== 'e' || b[1] !== props.note?.id);
if (!account || !props.note) return;
const aTag = ['a', `${props.note.msg.kind}:${props.note.pubkey}:${(props.note.msg.tags.find(t => t[0] === 'd') || [])[1]}`];
if (bookmarks.find(b => b[0] === aTag[0] && b[1] === aTag[1])) {
const bookmarksToAdd = bookmarks.filter(b => b[0] !== aTag[0] || b[1] !== aTag[1]);
if (bookmarksToAdd.length < 1) {
logWarning('BOOKMARK ISSUE: ', `before_bookmark_${APP_ID}`);

View File

@ -102,7 +102,7 @@
.articleSidebar {
.section {
margin-bottom: 28px;
max-height: 526px;
max-height: calc(100vh - 240px);
overflow-y: scroll;
/* Hide scrollbar for Chrome, Safari and Opera */

View File

@ -6,6 +6,19 @@
grid-template-columns: var(--left-col-w) var(--center-col-w) var(--right-col-w);
}
.containerLF {
--center-col-w: 680px;
--right-col-w: 308px;
--search-input-width: 260px;
--central-content-width: 680px;
width: var(--full-site-w);
margin: 0px auto;
display: grid;
grid-template-columns: var(--left-col-w) var(--center-col-w) var(--right-col-w);
}
.containerIOS {
z-index: 1;
margin-top: 54px;

View File

@ -97,6 +97,14 @@ const Layout: Component = () => {
account?.actions.checkNostrKey();
});
const containerClass = () => {
if (isIOS() && showBanner()) return styles.containerIOS;
if (location.pathname.startsWith('/e/naddr')) return styles.containerLF;
return styles.container;
}
return (
<Show
when={location.pathname !== '/'}
@ -119,7 +127,7 @@ const Layout: Component = () => {
</div>
<div id="modal" class={styles.modal}></div>
<BannerIOS />
<div id="container" ref={container} class={isIOS() && showBanner() ? styles.containerIOS : styles.container}>
<div id="container" ref={container} class={containerClass()}>
<div class={styles.leftColumn}>
<div>
<div id="branding_holder" class={styles.leftHeader}>

View File

@ -547,6 +547,44 @@
}
}
}
.doZaps {
position: relative;
display: flex;
justify-content: center;
align-items: center;
gap: 4px;
padding-inline: 10px;
padding-block: 2px;
margin: 0;
border-radius: 12px;
background: var(--text-primary);
width: fit-content;
min-height: 26px;
max-width: 100%;
text-decoration: none;
border: none;
outline: none;
.zapIcon {
width: 10px;
height: 12px;
background-color: var(--devider);
-webkit-mask: url(../../assets/icons/feed_zap_fill_2.svg) no-repeat 0 / 100%;
mask: url(../../assets/icons/feed_zap_fill_2.svg) no-repeat 0 / 100%;
}
.zapText {
color: var(--devider);
font-size: 12px;
font-weight: 700;
line-height: 12px;
}
&:hover {
background-color: var(--text-secondary);
}
}
}
.zapHighlightsCompact {

View File

@ -52,6 +52,7 @@ const Note: Component<{
noteType?: 'feed' | 'primary' | 'notification' | 'reaction' | 'thread'
onClick?: () => void,
quoteCount?: number,
size?: 'xwide' | 'wide' | 'normal' | 'short',
}> = (props) => {
const threadContext = useThreadContext();
@ -262,6 +263,8 @@ const Note: Component<{
updateReactionsState('topZaps', () => [ ...(threadContext?.topZaps[props.note.post.id] || []) ]);
});
const size = () => props.size ?? 'normal';
return (
<Switch>
<Match when={noteType() === 'notification'}>
@ -421,6 +424,7 @@ const Note: Component<{
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
onZapAnim={addTopZapFeed}
size={size()}
/>
</A>
</Match>

View File

@ -28,7 +28,7 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
const ArticleFooter: Component<{
note: PrimalArticle,
size?: 'wide' | 'normal' | 'short',
size?: 'xwide' | 'wide' | 'normal' | 'short',
id?: string,
state: NoteReactionsState,
updateState: SetStoreFunction<NoteReactionsState>,
@ -228,6 +228,11 @@ const ArticleFooter: Component<{
let newLeft = 33;
let newTop = -6;
if (size() === 'xwide') {
newLeft = 46;
newTop = -7;
}
if (size() === 'wide' && props.large) {
newLeft = 14;
newTop = -10;
@ -238,6 +243,8 @@ const ArticleFooter: Component<{
newTop = -6;
}
console.log('SIZE: ', newLeft);
medZapAnimation.style.left = `${newLeft}px`;
medZapAnimation.style.top = `${newTop}px`;

View File

@ -65,6 +65,16 @@
max-width: 20px;
}
&.xwide {
grid-template-columns: 153px 153px 153px 153px auto;
.bookmarkFoot {
display: flex;
justify-content: flex-end;
max-width: 100%;
}
}
&.wide {
grid-template-columns: 148px 148px 148px 148px auto;

View File

@ -27,7 +27,7 @@ export const lottieDuration = () => zapMD.op * 1_000 / zapMD.fr;
const NoteFooter: Component<{
note: PrimalNote,
size?: 'wide' | 'normal' | 'short',
size?: 'xwide' | 'wide' | 'normal' | 'short',
id?: string,
state: NoteReactionsState,
updateState: SetStoreFunction<NoteReactionsState>,
@ -228,6 +228,11 @@ const NoteFooter: Component<{
let newLeft = 33;
let newTop = -6;
if (size() === 'xwide') {
newLeft = 46;
newTop = -7;
}
if (size() === 'wide' && props.large) {
newLeft = 14;
newTop = -10;

View File

@ -11,7 +11,8 @@ const NoteTopZaps: Component<{
zapCount: number,
action: () => void,
id?: string,
users?: PrimalUser[]
users?: PrimalUser[],
doZap?: () => void,
}> = (props) => {
const threadContext = useThreadContext();
@ -45,8 +46,8 @@ const NoteTopZaps: Component<{
const zapSender = (zap: TopZap) => {
return (props.users || threadContext?.users || []).find(u => u.pubkey === zap.pubkey);
};
return (
return (
<Show
when={!threadContext?.isFetchingTopZaps}
fallback={
@ -64,49 +65,59 @@ const NoteTopZaps: Component<{
</div>
}
>
<div class={`${styles.zapHighlights}`}>
<TransitionGroup
name="top-zaps"
enterClass={styles.topZapEnterTransition}
exitClass={styles.topZapExitTransition}
>
<For each={topZaps()}>
{(zap, index) => (
<>
<button
class={`${styles.topZap}`}
onClick={() => props.action()}
style={`z-index: ${12 - index()};`}
>
<Avatar user={zapSender(zap)} size="micro" />
<div class={styles.amount}>
{zap.amount.toLocaleString()}
</div>
<Show when={index() === 0}>
<div class={styles.description}>
{zap.message}
<div class={`${styles.zapHighlights}`}>
<TransitionGroup
name="top-zaps"
enterClass={styles.topZapEnterTransition}
exitClass={styles.topZapExitTransition}
>
<For each={topZaps()}>
{(zap, index) => (
<>
<button
class={`${styles.topZap}`}
onClick={() => props.action()}
style={`z-index: ${12 - index()};`}
>
<Avatar user={zapSender(zap)} size="micro" />
<div class={styles.amount}>
{zap.amount.toLocaleString()}
</div>
<Show when={index() === 0}>
<div class={styles.description}>
{zap.message}
</div>
</Show>
</button>
<Show when={index() === 0 && props.topZaps.length > 3}>
<div class={styles.break}></div>
</Show>
</button>
</>
)}
</For>
<Show when={index() === 0 && props.topZaps.length > 3}>
<div class={styles.break}></div>
</Show>
</>
)}
</For>
<Show when={hasMoreZaps()}>
<button
class={styles.moreZaps}
onClick={() => props.action()}
>
<div class={styles.contextIcon}></div>
</button>
</Show>
<Show when={hasMoreZaps()}>
<button
class={styles.moreZaps}
onClick={() => props.action()}
>
<div class={styles.contextIcon}></div>
</button>
</Show>
</TransitionGroup>
</div>
</Show>
<Show when={props.doZap}>
<button
class={styles.doZaps}
onClick={props.doZap}
>
<div class={styles.zapIcon}></div>
<div class={styles.zapText}>Zap</div>
</button>
</Show>
</TransitionGroup>
</div>
</Show>
);
}

View File

@ -10,19 +10,19 @@
margin-block: 8px;
p {
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
margin-top: 0;
margin-bottom: 20px;
}
span {
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
}
h1 {
@ -95,10 +95,10 @@
content: '';
}
padding-left: 8px;
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
margin-top: 0;
margin-bottom: 12px;
@ -115,10 +115,10 @@
li {
padding-left: 8px;
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
margin-top: 0;
margin-bottom: 12px;
@ -135,18 +135,18 @@
margin-bottom: 20px;
dt {
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
}
dd {
padding-left: 8px;
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
margin-top: 0;
margin-bottom: 12px;
@ -170,10 +170,10 @@
blockquote {
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
padding-left: 12px;
padding-bottom: 0;
margin-top: 0;
@ -189,7 +189,7 @@
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
}
pre, code, mark {
@ -197,12 +197,12 @@
}
code {
color: var(--text-primary);
color: var(--brand-text);
font-family: "Fira Mono", monospace;
font-size: 16px;
font-style: normal;
font-weight: 400;
line-height: 24px;
line-height: 28px;
}
ins {
@ -218,7 +218,7 @@
color: var(--text-primary);
font-size: 16px;
font-weight: 700;
line-height: 24px;
line-height: 28px;
border-bottom: 1px solid var(--text-secondary);
padding: 12px 8px;
* {
@ -230,10 +230,10 @@
}
td {
color: var(--text-primary);
color: var(--brand-text);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
border-bottom: 1px solid var(--subtile-devider);
padding: 12px 8px;
* {

View File

@ -90,6 +90,7 @@ const account = useAccountContext();
return <NoteImage
class={`noteimage image_${props.noteId}`}
src={img.src}
width={640}
/>
}

View File

@ -4,7 +4,7 @@
align-items: center;
border-radius: 18px;
background-color: var(--background-header-input);
width: 300px;
width: var(--search-input-width);
height: 36px;
margin-left: 8px;
margin-bottom: 0;
@ -20,7 +20,7 @@
}
input {
width: 240px;
width: calc(var(--search-input-width) - 60px);
height: 36px;
font-weight: 700;
font-size: 16px;
@ -70,7 +70,7 @@
.searchHolder {
position: relative;
width: 316px;
width: calc(var(--search-input-width) + 16px);
padding-top: 8px;
padding-bottom: 8px;
border-radius: 12px;
@ -82,7 +82,7 @@
.searchSuggestions {
margin-top: 8px;
width: 316px;
width: calc(var(--search-input-width) + 16px);
visibility: visible;
&.hidden {

View File

@ -35,7 +35,7 @@
display: flex;
flex-direction: column;
justify-content: center;
max-width: 166px;
max-width: calc(var(--search-input-width) * 0.5);
}
.userName {

View File

@ -409,3 +409,5 @@ export const emptyInvoice: LnbcInvoice = {
expiry: 0,
route_hints: [],
};
export const supportedBookmarkTypes = ['a', 'e'];

View File

@ -23,7 +23,7 @@ import {
NostrEventContent,
PrimalArticle,
} from '../types/primal';
import { Kind, pinEncodePrefix, relayConnectingTimeout } from "../constants";
import { Kind, pinEncodePrefix, relayConnectingTimeout, supportedBookmarkTypes } from "../constants";
import { isConnected, refreshSocketListeners, removeSocketListeners, socket, subscribeTo, reset, subTo } from "../sockets";
import { sendContacts, sendLike, sendMuteList, triggerImportEvents } from "../lib/notes";
// @ts-ignore Bad types in nostr-tools
@ -1531,7 +1531,7 @@ export function AccountProvider(props: { children: JSXElement }) {
}
const notes = content.tags.reduce((acc, t) => {
if (t[0] === 'e') {
if (supportedBookmarkTypes.includes(t[0])) {
return [...acc, t[1]];
}
return [...acc];

View File

@ -68,6 +68,8 @@
--full-site-w: 1172px;
--header-height: 84px;
--search-input-width: 300px;
background-color: var(--background-site);
.mentioned_user {

View File

@ -3,6 +3,7 @@
justify-content: space-between;
align-items: center;
padding: 20px;
height: 85px;
border-bottom: 1px solid var(--devider);
a {
@ -89,9 +90,9 @@
.title {
color: var(--text-primary);
font-size: 36px;
font-size: 44px;
font-weight: 700;
line-height: 44px;
line-height: 52px;
}
.summary {
@ -109,7 +110,7 @@
color: var(--text-primary);
font-size: 16px;
font-weight: 400;
line-height: 24px;
line-height: 28px;
}
}

View File

@ -51,6 +51,7 @@ import { fetchNotes } from "../handleNotes";
import { Tier, TierCost } from "../components/SubscribeToAuthorModal/SubscribeToAuthorModal";
import ButtonPrimary from "../components/Buttons/ButtonPrimary";
import { zapSubscription } from "../lib/zap";
import Paginator from "../components/Paginator/Paginator";
export type LongFormData = {
title: string,
@ -106,199 +107,6 @@ const emptyStore: LongformThreadStore = {
hasTiers: false,
}
const test = `
# h1 Heading 8-)
## h2 Heading
### h3 Heading
#### h4 Heading
##### h5 Heading
###### h6 Heading
## Mentions
nostr:npub19f2765hdx8u9lz777w7azed2wsn9mqkf2gvn67mkldx8dnxvggcsmhe9da
nostr:note1tv033d7y088x8e90n5ut8htlsyy4yuwsw2fpgywq62w8xf0qcv8q8xvvhg
## Horizontal Rules
___
---
***
## Typographic replacements
Enable typographer option to see result.
(c) (C) (r) (R) (tm) (TM) (p) (P) +-
test.. test... test..... test?..... test!....
!!!!!! ???? ,, -- ---
"Smartypants, double quotes" and 'single quotes'
## Emphasis
**This is bold text**
__This is bold text__
*This is italic text*
_This is italic text_
~~Strikethrough~~
## Blockquotes
> Blockquotes can also be nested...
>> ...by using additional greater-than signs right next to each other...
> > > ...or with spaces between arrows.
## Lists
Unordered
+ Create a list by starting a line with \`+\`, \`-\`, or \`*\`
+ Sub-lists are made by indenting 2 spaces:
- Marker character change forces new list start:
* Ac tristique libero volutpat at
+ Facilisis in pretium nisl aliquet
- Nulla volutpat aliquam velit
+ Very easy!
Ordered
1. Lorem ipsum dolor sit amet
2. Consectetur adipiscing elit
3. Integer molestie lorem at massa
## Code
Inline \`code\`
Indented code
// Some comments
line 1 of code
line 2 of code
line 3 of code
Block code "fences"
\`\`\`
Sample text here...
\`\`\`
Syntax highlighting
\`\`\` js
var foo = function (bar) {
return bar++;
};
console.log(foo(5));
\`\`\`
## Tables
| Option | Description |
| ------ | ----------- |
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
Right aligned columns
| Option | Description |
| ------:| -----------:|
| data | path to data files to supply the data that will be passed into templates. |
| engine | engine to be used for processing templates. Handlebars is the default. |
| ext | extension to be used for dest files. |
## Links
[link text](http://dev.nodeca.com)
[link with title](http://nodeca.github.io/pica/demo/ "title text!")
Autoconverted link https://github.com/nodeca/pica (enable linkify to see)
## Images
![Minion](https://octodex.github.com/images/minion.png)
![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")[^image]
Like links, Images also have a footnote style syntax
With a reference later in the document defining the URL location, like this:
[^image]: https://octodex.github.com/images/dojocat.jpg "The Dojocat"
## Optionals (should we support them?)
### [Subscript](https://github.com/markdown-it/markdown-it-sub) / [Superscript](https://github.com/markdown-it/markdown-it-sup)
- 19^th^
- H~2~O
### [\<ins>](https://github.com/markdown-it/markdown-it-ins)
there is some ++Inserted text++ here
### [\<mark>](https://github.com/markdown-it/markdown-it-mark)
==Marked text==
### [Footnotes](https://github.com/markdown-it/markdown-it-footnote)
Footnote 1 link[^first].
Footnote 2 link[^second].
Duplicated footnote reference[^second].
[^first]: Footnote **can have markup**
and multiple paragraphs.
[^second]: Footnote text.
### [Definition lists](https://github.com/markdown-it/markdown-it-deflist)
Term 1
: Definition 1
with lazy continuation.
Term 2 with *inline markup*
: Definition 2
{ some code, part of Definition 2 }
Third paragraph of definition 2.
`;
const Longform: Component< { naddr: string } > = (props) => {
const account = useAccountContext();
const app = useAppContext();
@ -320,15 +128,31 @@ const Longform: Component< { naddr: string } > = (props) => {
let latestTopZapFeed: string = '';
let articleContextMenu: HTMLDivElement | undefined;
createEffect(() => {
const article = store.article;
if (!article) return;
const {
likes,
reposts,
replies,
zaps,
satszapped,
} = article;
updateReactionsState(() => ({ likes, reposts, replies, zapCount: zaps, satsZapped: satszapped }));
})
const [reactionsState, updateReactionsState] = createStore<NoteReactionsState>({
likes: 0,
likes: store.article?.likes || 0,
liked: false,
reposts: 0,
reposts: store.article?.reposts || 0,
reposted: false,
replies: 0,
replies: store.article?.replies || 0,
replied: false,
zapCount: 0,
satsZapped: 0,
zapCount: store.article?.zaps || 0,
satsZapped: store.article?.satszapped || 0,
zapped: false,
zappedAmount: 0,
zappedNow: false,
@ -844,6 +668,7 @@ const Longform: Component< { naddr: string } > = (props) => {
updateStore('article', () => ({ ...article }));
updateStore('isFetching', () => false);
// saveNotes(replies);
// const a = users.find(u => u.pubkey === article.author);
@ -909,7 +734,7 @@ const Longform: Component< { naddr: string } > = (props) => {
<A href={`/p/${store.article?.user.npub}`}>
<div class={styles.author}>
<Show when={store.article?.user}>
<Avatar user={store.article?.user} size="xs" />
<Avatar user={store.article?.user} size="sm" />
<div class={styles.userInfo}>
<div class={styles.userName}>
@ -984,7 +809,8 @@ const Longform: Component< { naddr: string } > = (props) => {
topZaps={store.article?.topZaps}
zapCount={reactionsState.zapCount}
users={store.users}
action={() => {}}
action={() => openReactionModal('zaps')}
doZap={() => app?.actions.openCustomZapModal(customZapInfo())}
/>
<PrimalMarkdown
@ -1016,6 +842,7 @@ const Longform: Component< { naddr: string } > = (props) => {
updateState={updateReactionsState}
customZapInfo={customZapInfo()}
onZapAnim={addTopZapFeed}
size="xwide"
/>
</div>
</Show>
@ -1030,7 +857,7 @@ const Longform: Component< { naddr: string } > = (props) => {
<div>
<For each={store.replies}>
{reply => <Note note={reply} />}
{reply => <Note note={reply} noteType='feed' size="xwide" />}
</For>
</div>
</>);