Skip to content

Commit

Permalink
feat: FollowSet
Browse files Browse the repository at this point in the history
  • Loading branch information
syusui-s committed Nov 28, 2024
1 parent 629c1f3 commit 748dc01
Show file tree
Hide file tree
Showing 13 changed files with 383 additions and 15 deletions.
6 changes: 6 additions & 0 deletions src/components/SideBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import Home from 'heroicons/24/outline/home.svg';
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
import Plus from 'heroicons/24/outline/plus.svg';
import User from 'heroicons/24/outline/user.svg';
import Users from 'heroicons/24/outline/users.svg';
import PencilSquare from 'heroicons/24/solid/pencil-square.svg';
import { ParseKeys } from 'i18next';
import throttle from 'lodash/throttle';
Expand Down Expand Up @@ -153,12 +154,17 @@ const columns: Readonly<Record<ColumnKind, { icon: string /* svg */; nameKey: Pa
icon: GlobeAlt,
nameKey: 'column.relay',
},
FollowSet: {
icon: Users,
nameKey: 'column.followSet',
},
Search: { icon: MagnifyingGlass, nameKey: 'column.search' },
};

const ColumnButton: Component<{ column: ColumnType; index: number }> = (props) => {
const i18n = useTranslation();

// eslint-disable-next-line no-unused-vars
const sortable = createSortable(props.column.id);

const request = useRequestCommand();
Expand Down
10 changes: 10 additions & 0 deletions src/components/column/Columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { For, Switch, Match } from 'solid-js';

import BookmarkColumn from '@/components/column/BookmarkColumn';
import FollowingColumn from '@/components/column/FollowingColumn';
import FollowSetColumn from '@/components/column/FollowSetColumn';
import NotificationColumn from '@/components/column/NotificationColumn';
import PostsColumn from '@/components/column/PostsColumn';
import ReactionsColumn from '@/components/column/ReactionsColumn';
Expand Down Expand Up @@ -65,6 +66,15 @@ const Columns = () => {
/>
)}
</Match>
<Match when={column.columnType === 'FollowSet' && column} keyed>
{(followSetColumn) => (
<FollowSetColumn
column={followSetColumn}
columnIndex={columnIndex()}
lastColumn={lastColumn()}
/>
)}
</Match>
<Match when={column.columnType === 'Bookmark' && column} keyed>
{(bookmarkColumn) => (
<BookmarkColumn
Expand Down
86 changes: 86 additions & 0 deletions src/components/column/FollowSetColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Component } from 'solid-js';

import Users from 'heroicons/24/outline/users.svg';
import uniq from 'lodash/uniq';

import BasicColumnHeader from '@/components/column/BasicColumnHeader';
import Column from '@/components/column/Column';
import ColumnSettings from '@/components/column/ColumnSettings';
import LoadMore, { useLoadMore } from '@/components/column/LoadMore';
import Timeline from '@/components/timeline/Timeline';
import { FollowSetColumnType } from '@/core/column';
import { applyContentFilter } from '@/core/contentFilter';
import useConfig from '@/core/useConfig';
import { useTranslation } from '@/i18n/useTranslation';
import useFollowSet from '@/nostr/useFollowSet';
import useSubscription from '@/nostr/useSubscription';

type FollowingColumnDisplayProps = {
columnIndex: number;
lastColumn: boolean;
column: FollowSetColumnType;
};

const FollowingColumn: Component<FollowingColumnDisplayProps> = (props) => {
const i18n = useTranslation();
const { config, removeColumn } = useConfig();

const { followSet, title, pubkeys } = useFollowSet(() => ({
author: props.column.author,
identifier: props.column.identifier,
}));

const columnName = () =>
props.column.name || title() || followSet()?.identifier() || i18n.t('column.followSet');

const loadMore = useLoadMore(() => ({ duration: null }));

const { events, eose } = useSubscription(() => {
const authors = uniq([...pubkeys()]);
if (authors.length === 0) return null;
return {
debugId: 'following',
relayUrls: config().relayUrls,
filters: [
{
kinds: [1, 6],
authors,
limit: 20,
since: loadMore.since(),
until: loadMore.until(),
},
],
eoseLimit: 20,
continuous: loadMore.continuous(),
onEOSE: () => {
console.log('home: eose');
},
clientEventFilter: (event) => {
if (props.column.contentFilter == null) return true;
return applyContentFilter(props.column.contentFilter)(event.content);
},
};
});

return (
<Column
header={
<BasicColumnHeader
name={columnName()}
icon={<Users />}
settings={() => <ColumnSettings column={props.column} columnIndex={props.columnIndex} />}
onClose={() => removeColumn(props.column.id)}
/>
}
width={props.column.width}
columnIndex={props.columnIndex}
lastColumn={props.lastColumn}
>
<LoadMore loadMore={loadMore} eose={eose()}>
<Timeline events={events()} />
</LoadMore>
</Column>
);
};

export default FollowingColumn;
16 changes: 16 additions & 0 deletions src/components/modal/AddColumn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import Home from 'heroicons/24/outline/home.svg';
import MagnifyingGlass from 'heroicons/24/outline/magnifying-glass.svg';
import Server from 'heroicons/24/outline/server.svg';
import User from 'heroicons/24/outline/user.svg';
import Users from 'heroicons/24/outline/users.svg';
// import BookmarkIcon from 'heroicons/24/outline/bookmark.svg';
// import ChatBubbleLeftRight from 'heroicons/24/outline/chat-bubble-left-right.svg';

import BasicModal from '@/components/modal/BasicModal';
import FollowSetsDisplay from '@/components/modal/followset/FollowSetsDisplay';
import {
createFollowingColumn,
createJapanRelaysColumn,
createNotificationColumn,
createPostsColumn,
createReactionsColumn,
createRelaysColumn,
createFollowSetColumn,
createSearchColumn,
} from '@/core/column';
import useConfig from '@/core/useConfig';
Expand Down Expand Up @@ -131,6 +134,11 @@ const AddColumn: Component<AddColumnProps> = (props) => {
finish();
};

const addFollowSetColumn = (author: string, identifier: string) => {
saveColumn(createFollowSetColumn({ author, identifier }));
finish();
};

const addSearchColumn = () => {
saveColumn(createSearchColumn({ query: '' }));
finish();
Expand Down Expand Up @@ -166,6 +174,11 @@ const AddColumn: Component<AddColumnProps> = (props) => {
icon: () => <Server />,
onSelect: () => setDetailComponent('AddRelaysColumn'),
},
{
name: () => i18n.t('column.followSet'),
icon: () => <Users />,
onSelect: () => setDetailComponent('AddFollowSet'),
},
{
name: () => i18n.t('column.japanese'),
icon: () => <GlobeAlt />,
Expand Down Expand Up @@ -212,6 +225,9 @@ const AddColumn: Component<AddColumnProps> = (props) => {
<Match when={detailComponent() === 'AddRelaysColumn'}>
<AddRelaysColumn addRelaysColumn={addRelaysColumn} />
</Match>
<Match when={detailComponent() === 'AddFollowSet'}>
<FollowSetsDisplay pubkey={pubkey()} onSelectFollowSet={addFollowSetColumn} />
</Match>
</Switch>
</BasicModal>
);
Expand Down
54 changes: 54 additions & 0 deletions src/components/modal/followset/FollowSetsDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { type Component, For, Show } from 'solid-js';

import { useTranslation } from '@/i18n/useTranslation';
import NostrSet from '@/nostr/event/sets/NostrSet';
import useFollowSets from '@/nostr/useFollowSets';
import ensureNonNull from '@/utils/ensureNonNull';

type FollowSetsDisplayProps = {
pubkey?: string;
onSelectFollowSet: (pubkey: string, identifier: string) => void;
};

const FollowSetsDisplay: Component<FollowSetsDisplayProps> = (props) => {
const i18n = useTranslation();

const { followSets } = useFollowSets(() =>
ensureNonNull([props.pubkey] as const)(([pubkeyNonNull]) => ({
author: pubkeyNonNull,
})),
);

return (
<div class="p-8">
{i18n.t('column.addFollowSetColumn.numberOfFollowSets', { count: followSets().length })}
<div class="flex flex-col divide-y divide-border rounded border border-border">
<For each={followSets()}>
{(followSet) => {
const event = new NostrSet(followSet);

return (
<button
type="button"
class="flex items-center gap-2 p-2 hover:bg-bg-tertiary"
onClick={() => props.onSelectFollowSet(event.pubkey, event.identifier())}
>
<div class="size-8 shrink-0 bg-fg-secondary">
<Show when={event.image()} keyed>
{(url) => <img class="size-full object-cover" src={url} alt="icon" />}
</Show>
</div>
<div class="shrink-0 grow truncate text-start">
{event.title() ?? event.identifier()}
</div>
<div class="px-2 text-fg-secondary">{event.pTags().length}</div>
</button>
);
}}
</For>
</div>
</div>
);
};

export default FollowSetsDisplay;
17 changes: 17 additions & 0 deletions src/core/column.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,14 @@ export const RelaysColumnSchema = BaseColumnSchema.extend({
});
export type RelaysColumnType = z.infer<typeof RelaysColumnSchema>;

/** A column which shows text notes and reposts posted to the specific relays */
export const FollowSetColumnSchema = BaseColumnSchema.extend({
columnType: z.literal('FollowSet'),
author: z.string(),
identifier: z.string(),
});
export type FollowSetColumnType = z.infer<typeof FollowSetColumnSchema>;

/** A column which search text notes from relays which support NIP-50 */
export const SearchColumnSchema = BaseColumnSchema.extend({
columnType: z.literal('Search'),
Expand Down Expand Up @@ -124,6 +132,7 @@ export const ColumnTypeSchema = z.union([
ReactionsColumnSchema,
ChannelColumnSchema,
RelaysColumnSchema,
FollowSetColumnSchema,
SearchColumnSchema,
BookmarkColumnSchema,
]);
Expand Down Expand Up @@ -160,6 +169,14 @@ export const createRelaysColumn = (params: CreateParams<RelaysColumnType>): Rela
...params,
});

export const createFollowSetColumn = (
params: CreateParams<FollowSetColumnType>,
): FollowSetColumnType => ({
...createBaseColumn(),
columnType: 'FollowSet',
...params,
});

export const createJapanRelaysColumn = () =>
createRelaysColumn({
name: '日本語',
Expand Down
6 changes: 5 additions & 1 deletion src/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export default {
home: 'Home',
notification: 'Notification',
relay: 'Relay',
followSet: 'Follow set',
japanese: 'Japanese',
posts: 'User',
reactions: 'Reactions',
Expand All @@ -41,7 +42,10 @@ export default {
loadLatest: 'Load latest posts',
loadOld: 'Load old posts',
addRelayColumn: {
add: '追加',
add: 'Add',
},
addFollowSetColumn: {
numberOfFollowSets: '{{count}} followsets',
},
config: {
columnWidth: 'Column width',
Expand Down
4 changes: 4 additions & 0 deletions src/locales/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default {
home: 'ホーム',
notification: '通知',
relay: 'リレー',
followSet: 'フォローセット',
japanese: '日本語',
posts: '投稿',
reactions: 'リアクション',
Expand All @@ -42,6 +43,9 @@ export default {
addRelayColumn: {
add: '追加',
},
addFollowSetColumn: {
numberOfFollowSets: '{{count}} 件',
},
config: {
columnWidth: 'カラム幅',
widest: '特大',
Expand Down
21 changes: 21 additions & 0 deletions src/nostr/useCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,27 @@ export const signEvent = async (unsignedEvent: UnsignedEvent): Promise<NostrEven
return signedEvent;
};

export const encrypt = async (pubkey: string, content: string): Promise<string> => {
if (window.nostr == null) {
throw new Error('NIP-07 implementation not found');
}
if (window.nostr.nip04 == null) {
throw new Error('NIP-04 implementation not found');
}
return window.nostr.nip04.encrypt(pubkey, content);
};

export const decrypt = async (pubkey: string, encryptedContent: string): Promise<string> => {
if (window.nostr == null) {
throw new Error('NIP-07 implementation not found');
}
if (window.nostr.nip04 == null) {
throw new Error('NIP-04 implementation not found');
}

return window.nostr.nip04.decrypt(pubkey, encryptedContent);
};

const useCommands = () => {
const pool = usePool();

Expand Down
Loading

0 comments on commit 748dc01

Please sign in to comment.