sync deletion events (#455)

This commit is contained in:
Bob 2024-05-14 17:52:02 +08:00 committed by GitHub
parent f9d3f88cfb
commit 049b2d56c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 212 additions and 46 deletions

View File

@ -29,12 +29,13 @@ import { PublicMessageContainer } from "./public-message-container.tsx";
import { ChatMessage } from "./message.ts";
import { filter, forever, map } from "./_helper.ts";
import { RightPanel } from "./components/right-panel.tsx";
import { ComponentChildren } from "https://esm.sh/preact@10.17.1";
import { SignIn } from "./sign-in.tsx";
import { getTags, Parsed_Event } from "../nostr.ts";
import { Toast } from "./components/toast.tsx";
import { ToastChannel } from "./components/toast.tsx";
import { RightPanelChannel } from "./components/right-panel.tsx";
import { getRelayInformation } from "./relay-detail.tsx";
import { func_IsAdmin } from "./message-list.tsx";
export async function Start(database: DexieDatabase) {
console.log("Start the application");
@ -56,6 +57,7 @@ export async function Start(database: DexieDatabase) {
const toastInputChan: ToastChannel = new Channel();
const dbView = await Database_View.New(database, database, database);
const newNostrEventChannel = new Channel<NostrEvent>();
(async () => {
for await (const event of newNostrEventChannel) {
const err = await pool.sendEvent(event);
@ -246,6 +248,7 @@ export class App {
forever(sync_client_specific_data(this.pool, this.ctx, this.database));
forever(sync_profile_events(this.database, this.pool));
forever(sync_public_notes(this.pool, this.database));
forever(sync_deletion_events(this.pool, this.database));
}
(async () => {
@ -315,7 +318,24 @@ type AppProps = {
installPrompt: InstallPrompt;
};
export class AppComponent extends Component<AppProps> {
export class AppComponent extends Component<AppProps, {
isAdmin: func_IsAdmin | undefined;
admin: string | undefined;
}> {
state = {
isAdmin: undefined,
admin: undefined,
};
async componentDidMount() {
await this.updateAdminState();
for await (const update of this.props.eventBus.onChange()) {
if (update.type == "SelectRelay") {
this.updateAdminState();
}
}
}
render(props: AppProps) {
const t = Date.now();
const model = props.model;
@ -350,6 +370,7 @@ export class AppComponent extends Component<AppProps> {
getProfilesByText: app.database.getProfilesByText,
isUserBlocked: app.conversationLists.isUserBlocked,
getEventByID: app.database.getEventByID,
isAdmin: this.state.isAdmin,
}}
userBlocker={app.conversationLists}
/>
@ -376,6 +397,7 @@ export class AppComponent extends Component<AppProps> {
getProfilesByText: app.database.getProfilesByText,
isUserBlocked: app.conversationLists.isUserBlocked,
getEventByID: app.database.getEventByID,
isAdmin: this.state.isAdmin,
}}
messages={Array.from(
map(
@ -386,7 +408,8 @@ export class AppComponent extends Component<AppProps> {
return false;
}
const relays = app.database.getRelayRecord(e.id);
return relays.has(model.currentRelay);
return relays.has(model.currentRelay) &&
!app.database.isDeleted(e.id, this.state.admin);
},
),
(e) => {
@ -464,6 +487,22 @@ export class AppComponent extends Component<AppProps> {
console.debug("AppComponent:end", Date.now() - t);
return final;
}
updateAdminState = async () => {
const currentRelayInformation = await getRelayInformation(this.props.model.currentRelay);
if (currentRelayInformation instanceof Error) {
console.error(currentRelayInformation);
return;
}
this.setState({
admin: currentRelayInformation.pubkey,
isAdmin: this.isAdmin(currentRelayInformation.pubkey),
});
};
isAdmin = (admin?: string) => (pubkey: string) => {
return admin === pubkey;
};
}
// todo: move to somewhere else
@ -573,6 +612,33 @@ const sync_client_specific_data = async (
}
};
const sync_deletion_events = async (
pool: ConnectionPool,
database: Database_View,
) => {
const stream = await pool.newSub("sync_deletion_events", {
kinds: [NostrKind.DELETE],
since: hours_ago(48),
});
if (stream instanceof Error) {
return stream;
}
for await (const msg of stream.chan) {
if (msg.res.type === "EOSE") {
continue;
} else if (msg.res.type === "NOTICE") {
console.log(`Notice: ${msg.res.note}`);
continue;
}
const ok = await database.addEvent(msg.res.event, msg.url);
if (ok instanceof Error) {
console.error(msg);
console.error(ok);
}
}
};
export function hours_ago(hours: number) {
return Math.floor(Date.now() / 1000) - hours * 60 * 60;
}

View File

@ -1,10 +1,7 @@
import { NavigationModel } from "./nav.tsx";
import { ProfileData } from "../features/profile.ts";
import { DM_Model } from "./dm.tsx";
import { Public_Model } from "./public-message-container.tsx";
import { App } from "./app.tsx";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { default_blowater_relay } from "./relay-config.ts";
export type Model = {

View File

@ -5,7 +5,7 @@ import {
PutChannel,
sleep,
} from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts";
import { prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts";
import { prepareDeletionEvent, prepareNormalNostrEvent } from "../../libs/nostr.ts/event.ts";
import { prepareReplyEvent } from "../nostr.ts";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
@ -55,7 +55,7 @@ import { SendingEventRejection, ToastChannel } from "./components/toast.tsx";
import { SingleRelayConnection } from "../../libs/nostr.ts/relay-single.ts";
import { default_blowater_relay } from "./relay-config.ts";
import { forever } from "./_helper.ts";
import { func_GetEventByID } from "./message-list.tsx";
import { DeleteEvent, func_GetEventByID } from "./message-list.tsx";
import { FilterContent } from "./filter.tsx";
import { CloseRightPanel } from "./components/right-panel.tsx";
import { RightPanelChannel } from "./components/right-panel.tsx";
@ -86,7 +86,8 @@ export type UI_Interaction_Event =
| FilterContent
| CloseRightPanel
| ReplyToMessage
| EditorSelectProfile;
| EditorSelectProfile
| DeleteEvent;
type BackToContactList = {
type: "BackToContactList";
@ -317,6 +318,25 @@ const handle_update_event = async (chan: PutChannel<true>, args: {
app.rightPanelInputChan.put(undefined);
} //
//
// Deletion
//
else if (event.type == "DeleteEvent") {
const deletionEvent = await prepareDeletionEvent(
app.ctx,
"Request deletion",
event.event,
);
if (deletionEvent instanceof Error) {
app.toastInputChan.put(() => deletionEvent.message);
continue;
}
const err = await current_relay.sendEvent(deletionEvent);
if (err instanceof Error) {
app.toastInputChan.put(() => err.message);
continue;
}
} //
//
// DM
//
else if (event.type == "ViewUserDetail") {

View File

@ -9,7 +9,7 @@ import { IconButtonClass } from "./components/tw.ts";
import { LeftArrowIcon } from "./icons/left-arrow-icon.tsx";
import { MessagePanel_V0 } from "./message-panel.tsx";
import { func_GetProfileByPublicKey, func_GetProfilesByText, ProfileGetter } from "./search.tsx";
import { func_GetProfileByPublicKey, func_GetProfilesByText } from "./search.tsx";
import {
ConversationList,
@ -17,7 +17,7 @@ import {
NewMessageChecker,
PinListGetter,
} from "./conversation-list.tsx";
import { func_GetEventByID } from "./message-list.tsx";
import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx";
export type DM_Model = {
currentConversation: PublicKey | undefined;
@ -36,6 +36,7 @@ type DirectMessageContainerProps = {
relayRecordGetter: RelayRecordGetter;
isUserBlocked: (pubkey: PublicKey) => boolean;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
};
userBlocker: UserBlocker;
} & DM_Model;

View File

@ -13,18 +13,17 @@ export function DeleteIcon(props: {
<svg
class={props.class}
style={props.style}
viewBox="0 0 16 16"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g>
<path
d="M10.6667 4.00016V3.46683C10.6667 2.72009 10.6667 2.34672 10.5213 2.06151C10.3935 1.81063 10.1895 1.60665 9.93865 1.47882C9.65344 1.3335 9.28007 1.3335 8.53333 1.3335H7.46667C6.71993 1.3335 6.34656 1.3335 6.06135 1.47882C5.81046 1.60665 5.60649 1.81063 5.47866 2.06151C5.33333 2.34672 5.33333 2.72009 5.33333 3.46683V4.00016M6.66667 7.66683V11.0002M9.33333 7.66683V11.0002M2 4.00016H14M12.6667 4.00016V11.4668C12.6667 12.5869 12.6667 13.147 12.4487 13.5748C12.2569 13.9511 11.951 14.2571 11.5746 14.4488C11.1468 14.6668 10.5868 14.6668 9.46667 14.6668H6.53333C5.41323 14.6668 4.85318 14.6668 4.42535 14.4488C4.04903 14.2571 3.74307 13.9511 3.55132 13.5748C3.33333 13.147 3.33333 12.5869 3.33333 11.4668V4.00016"
stroke-width="1.33333"
stroke-linecap="round"
stroke-linejoin="round"
/>
</g>
<path
d="M16 6V5.2C16 4.0799 16 3.51984 15.782 3.09202C15.5903 2.71569 15.2843 2.40973 14.908 2.21799C14.4802 2 13.9201 2 12.8 2H11.2C10.0799 2 9.51984 2 9.09202 2.21799C8.71569 2.40973 8.40973 2.71569 8.21799 3.09202C8 3.51984 8 4.0799 8 5.2V6M10 11.5V16.5M14 11.5V16.5M3 6H21M19 6V17.2C19 18.8802 19 19.7202 18.673 20.362C18.3854 20.9265 17.9265 21.3854 17.362 21.673C16.7202 22 15.8802 22 14.2 22H9.8C8.11984 22 7.27976 22 6.63803 21.673C6.07354 21.3854 5.6146 20.9265 5.32698 20.362C5 19.7202 5 18.8802 5 17.2V6"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
);
}

View File

@ -29,23 +29,32 @@ import { AboutIcon } from "./icons/about-icon.tsx";
import { BackgroundColor_MessagePanel, PrimaryTextColor } from "./style/colors.ts";
import { Parsed_Event } from "../nostr.ts";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
import { robohash } from "./relay-detail.tsx";
import { RelayInformation, robohash } from "./relay-detail.tsx";
import { ReplyIcon } from "./icons/reply-icon.tsx";
import { ChatMessagesGetter } from "./app_update.tsx";
import { NostrKind } from "../../libs/nostr.ts/nostr.ts";
import { func_GetProfileByPublicKey } from "./search.tsx";
import { DeleteIcon } from "./icons/delete-icon.tsx";
export type func_IsAdmin = (pubkey: string) => boolean;
interface Props {
myPublicKey: PublicKey;
messages: ChatMessage[];
emit: emitFunc<
DirectMessagePanelUpdate | SelectConversation | SyncEvent | ViewUserDetail | ReplyToMessage
| DirectMessagePanelUpdate
| SelectConversation
| SyncEvent
| ViewUserDetail
| ReplyToMessage
| DeleteEvent
>;
getters: {
messageGetter: ChatMessagesGetter;
getProfileByPublicKey: func_GetProfileByPublicKey;
relayRecordGetter: RelayRecordGetter;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
};
}
@ -278,16 +287,23 @@ function MessageBoxGroup(props: {
messages: ChatMessage[];
myPublicKey: PublicKey;
emit: emitFunc<
DirectMessagePanelUpdate | ViewUserDetail | SelectConversation | SyncEvent | ReplyToMessage
| DirectMessagePanelUpdate
| ViewUserDetail
| SelectConversation
| SyncEvent
| ReplyToMessage
| DeleteEvent
>;
getters: {
messageGetter: ChatMessagesGetter;
getProfileByPublicKey: func_GetProfileByPublicKey;
relayRecordGetter: RelayRecordGetter;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
};
}) {
const first_message = props.messages[0];
const { myPublicKey } = props;
const rows = [];
rows.push(
@ -296,7 +312,12 @@ function MessageBoxGroup(props: {
isMobile() ? "select-none" : ""
}`}
>
{MessageActions(first_message, props.emit)}
{MessageActions({
isAdmin: props.getters.isAdmin,
myPublicKey,
message: first_message,
emit: props.emit,
})}
{renderRelply(first_message.event, props.getters, props.emit)}
<div class="flex items-start">
<Avatar
@ -345,7 +366,12 @@ function MessageBoxGroup(props: {
isMobile() ? "select-none" : ""
}`}
>
{MessageActions(msg, props.emit)}
{MessageActions({
isAdmin: props.getters.isAdmin,
myPublicKey,
message: msg,
emit: props.emit,
})}
{Time(msg.created_at)}
<div
class={`flex-1`}
@ -377,10 +403,13 @@ export type ReplyToMessage = {
event: Parsed_Event;
};
function MessageActions(
message: ChatMessage,
emit: emitFunc<DirectMessagePanelUpdate | ReplyToMessage>,
) {
function MessageActions(args: {
myPublicKey: PublicKey;
message: ChatMessage;
emit: emitFunc<DirectMessagePanelUpdate | ReplyToMessage | DeleteEvent>;
isAdmin: func_IsAdmin | undefined;
}) {
const { myPublicKey, message, emit, isAdmin } = args;
return (
<div
class={`hidden
@ -405,6 +434,24 @@ function MessageActions(
<ReplyIcon class={`w-5 h-5 text-[#B6BAC0] hover:text-[#D9DBDE]`} />
</button>
{(myPublicKey.hex === message.author.hex || (isAdmin && isAdmin(myPublicKey.hex))) &&
(message.event.kind === NostrKind.TEXT_NOTE) &&
(
<button
class={`flex items-center justify-center
p-1
bg-[#313338] hover:bg-[#3A3C41]`}
onClick={() => {
emit({
type: "DeleteEvent",
event: message.event,
});
}}
>
<DeleteIcon class={`w-5 h-5 text-[#B6BAC0] hover:text-[#D9DBDE]`} />
</button>
)}
<button
class={`flex items-center justify-center
p-1
@ -422,6 +469,11 @@ function MessageActions(
);
}
export type DeleteEvent = {
type: "DeleteEvent";
event: Parsed_Event;
};
function last<T>(array: Array<T>): T | undefined {
if (array.length == 0) {
return undefined;

View File

@ -23,7 +23,13 @@ import {
LinkColor,
} from "./style/colors.ts";
import { BlockUser, UnblockUser } from "./user-detail.tsx";
import { func_GetEventByID, MessageList, ReplyToMessage } from "./message-list.tsx";
import {
DeleteEvent,
func_GetEventByID,
func_IsAdmin,
MessageList,
ReplyToMessage,
} from "./message-list.tsx";
import { MessageList_V0 } from "./message-list.tsx";
import { func_GetProfileByPublicKey } from "./search.tsx";
@ -47,7 +53,6 @@ export type ViewUserDetail = {
interface MessagePanelProps {
myPublicKey: PublicKey;
emit: emitFunc<
| EditorEvent
| DirectMessagePanelUpdate
@ -58,6 +63,7 @@ interface MessagePanelProps {
| UnblockUser
| SyncEvent
| ReplyToMessage
| DeleteEvent
>;
eventSub: EventSubscriber<UI_Interaction_Event>;
messages: ChatMessage[];
@ -68,6 +74,7 @@ interface MessagePanelProps {
relayRecordGetter: RelayRecordGetter;
isUserBlocked: (pubkey: PublicKey) => boolean;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
};
}

View File

@ -23,7 +23,6 @@ import { RelaySwitchList } from "./relay-switch-list.tsx";
import { SocialIcon } from "./icons/social-icon.tsx";
import { SearchIcon } from "./icons/search-icon.tsx";
import { StartSearch } from "./search_model.ts";
import { setState } from "./_helper.ts";
export type InstallPrompt = {
event: Event | undefined;
@ -49,8 +48,8 @@ type Props = {
emit: emitFunc<NavigationUpdate | SelectRelay | StartSearch>;
installPrompt: InstallPrompt;
pool: ConnectionPool;
currentRelay?: string;
activeNav: NavTabID;
currentRelay: string;
};
type State = {
@ -140,8 +139,13 @@ export class NavBar extends Component<Props, State> {
render(props: Props) {
return (
<div class={this.styles.container}>
{/* <Avatar class={this.styles.avatar} picture={this.props.profile?.profile?.picture} /> */}
{<RelaySwitchList emit={props.emit} pool={props.pool} currentRelay={props.currentRelay} />}
{
<RelaySwitchList
emit={props.emit}
pool={props.pool}
currentRelay={props.currentRelay}
/>
}
{this.tabs.map(({ icon, id }) => (
<div class={this.styles.tabsContainer}>
{id === "Setting" && this.state.installPrompt.event

View File

@ -3,7 +3,7 @@ import { SingleRelayConnection } from "../../libs/nostr.ts/relay-single.ts";
import { emitFunc, EventBus } from "../event-bus.ts";
import { ChatMessagesGetter, UI_Interaction_Event } from "./app_update.tsx";
import { setState } from "./_helper.ts";
import { func_GetProfileByPublicKey, func_GetProfilesByText, ProfileGetter } from "./search.tsx";
import { func_GetProfileByPublicKey, func_GetProfilesByText } from "./search.tsx";
import { RelayRecordGetter } from "../database.ts";
import { NewMessageChecker } from "./conversation-list.tsx";
@ -13,7 +13,7 @@ import { NostrAccountContext } from "../../libs/nostr.ts/nostr.ts";
import { MessagePanel } from "./message-panel.tsx";
import { PublicKey } from "../../libs/nostr.ts/key.ts";
import { ChatMessage } from "./message.ts";
import { func_GetEventByID } from "./message-list.tsx";
import { func_GetEventByID, func_IsAdmin } from "./message-list.tsx";
import { Filter, FilterContent } from "./filter.tsx";
import { NoteID } from "../../libs/nostr.ts/nip19.ts";
@ -37,6 +37,7 @@ type Props = {
relayRecordGetter: RelayRecordGetter;
isUserBlocked: func_IsUserBlocked;
getEventByID: func_GetEventByID;
isAdmin: func_IsAdmin | undefined;
};
} & Public_Model;

View File

@ -11,9 +11,9 @@ import { setState } from "./_helper.ts";
import { AddIcon } from "./icons/add-icon.tsx";
type RelaySwitchListProps = {
currentRelay?: string;
pool: ConnectionPool;
emit: emitFunc<SelectRelay | NavigationUpdate>;
currentRelay: string;
};
type RelaySwitchListState = {

View File

@ -248,10 +248,7 @@ export class RelaySetting extends Component<RelaySettingProp, RelaySettingState>
onClick={this.removeRelay(props, r.url)}
>
<DeleteIcon
class={`w-[1rem] h-[1rem]`}
style={{
stroke: ErrorColor,
}}
class={`w-[1rem] h-[1rem] text-[${ErrorColor}]`}
/>
</button>
)

View File

@ -65,6 +65,7 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
private readonly sourceOfChange = csp.chan<{ event: Parsed_Event; relay?: string }>(buffer_size);
private readonly caster = csp.multi<{ event: Parsed_Event; relay?: string }>(this.sourceOfChange);
private readonly profiles = new Map<string, Profile_Nostr_Event>();
private readonly deletionEvents = new Map</* event id */ string, /* deletion event */ Parsed_Event>();
private constructor(
private readonly eventsAdapter: EventsAdapter,
@ -125,18 +126,22 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
new Set(all_removed_events.map((mark) => mark.event_id)),
);
console.log("Datebase_View:New time spent", Date.now() - t);
for (const e of db.events.values()) {
if (e.kind == NostrKind.META_DATA) {
for (const event of db.events.values()) {
if (event.kind == NostrKind.META_DATA) {
// @ts-ignore
const pEvent = parseProfileEvent(e);
const pEvent = parseProfileEvent(event);
if (pEvent instanceof Error) {
console.error(pEvent);
continue;
}
db.setProfile(pEvent);
} else if (event.kind == NostrKind.DELETE) {
event.parsedTags.e.forEach((event_id) => {
db.deletionEvents.set(event_id, event);
});
}
}
console.log(`Datebase_View:Deletion events size: ${db.deletionEvents.size}`);
return db;
}
@ -159,6 +164,19 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
}
}
isDeleted(id: string, admin?: string) {
const deletionEvent = this.deletionEvents.get(id);
if (deletionEvent == undefined) {
return false;
}
const targetEvent = this.getEventByID(id);
if (targetEvent == undefined) {
return false;
}
return deletionEvent.pubkey == targetEvent.publicKey.hex ||
deletionEvent.pubkey == admin;
}
async remove(id: string): Promise<void> {
this.removedEvents.add(id);
await this.eventMarker.markEvent(id, "removed");
@ -274,6 +292,10 @@ export class Database_View implements ProfileSetter, ProfileGetter, EventRemover
return pEvent;
}
this.setProfile(pEvent);
} else if (parsedEvent.kind == NostrKind.DELETE) {
parsedEvent.parsedTags.e.forEach((event_id) => {
this.deletionEvents.set(event_id, parsedEvent);
});
}
await this.eventsAdapter.put(event);