Add ChannelMessageMenuMobile

This commit is contained in:
Jon Staab
2024-10-24 15:48:45 -07:00
parent 89e4367208
commit a5173df121
24 changed files with 170 additions and 76 deletions
+13 -6
View File
@@ -4,6 +4,7 @@
import type {TrustedEvent} from "@welshman/util"
import {deriveProfile, deriveProfileDisplay, formatTimestampAsTime, pubkey} from "@welshman/app"
import type {Thunk} from "@welshman/app"
import {isMobile} from "@lib/html"
import {slideAndFade, conditionalTransition} from "@lib/transition"
import Delay from "@lib/components/Delay.svelte"
import Avatar from "@lib/components/Avatar.svelte"
@@ -15,9 +16,10 @@
import ChannelThread from "@app/components/ChannelThread.svelte"
import ChannelMessageEmojiButton from "@app/components/ChannelMessageEmojiButton.svelte"
import ChannelMessageMenuButton from "@app/components/ChannelMessageMenuButton.svelte"
import ChannelMessageMenuMobile from "@app/components/ChannelMessageMenuMobile.svelte"
import {colors, tagRoom, deriveEvent, pubkeyLink} from "@app/state"
import {publishDelete, publishReaction} from "@app/commands"
import {pushDrawer} from "@app/modal"
import {pushDrawer, pushModal} from "@app/modal"
export let url, room
export let event: TrustedEvent
@@ -35,10 +37,14 @@
const transition = conditionalTransition(thunk, slideAndFade)
const openThread = () => {
const root = $rootEvent || event
const onClick = () => {
if (isMobile) {
pushModal(ChannelMessageMenuMobile, {url, event})
} else {
const root = $rootEvent || event
pushDrawer(ChannelThread, {url, room, event: root})
pushDrawer(ChannelThread, {url, room, event: root})
}
}
const onReactionClick = (content: string, events: TrustedEvent[]) => {
@@ -60,7 +66,7 @@
<Delay>
<button
in:transition
on:click={openThread}
on:click={onClick}
type="button"
class="group relative flex w-full flex-col gap-1 p-2 text-left transition-colors hover:bg-base-300">
<div class="flex w-full gap-3">
@@ -102,7 +108,8 @@
<ReactionSummary {event} {onReactionClick} />
</div>
<button
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all group-hover:opacity-100"
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
class:group-hover:opacity-100={!isMobile}
on:click|stopPropagation>
<ChannelMessageEmojiButton {url} {room} {event} />
<ChannelMessageMenuButton {url} {event} />
@@ -1,6 +1,7 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import Icon from "@lib/components/Icon.svelte"
import {tagRoom} from "@app/state"
import {publishReaction} from "@app/commands"
@@ -15,4 +16,6 @@
})
</script>
<EmojiButton {onEmoji} class="join-item" />
<EmojiButton {onEmoji} class="btn join-item btn-xs">
<Icon icon="smile-circle" size={4} />
</EmojiButton>
+2 -14
View File
@@ -2,9 +2,8 @@
import {pubkey} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import {publishDelete} from "@app/commands"
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
import {pushModal} from "@app/modal"
export let url
@@ -18,18 +17,7 @@
const showDelete = () => {
onClick()
pushModal(Confirm, {
title: "Delete Message",
subtitle: "Are you sure you want to delete this message?",
message: `
This will send a request to delete this message.
Be aware that not all relays may honor this request.`,
confirm: async () => {
await publishDelete({event, relays: [url]})
history.back()
},
})
pushModal(ConfirmDelete, {url, event})
}
</script>
@@ -0,0 +1,56 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import {pubkey} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ChannelThread from "@app/components/ChannelThread.svelte"
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
import {publishReaction} from "@app/commands"
import {pushModal, pushDrawer} from "@app/modal"
import {tagRoom} from "@app/state"
export let url
export let room
export let event
const onEmoji = (emoji: NativeEmoji) => {
history.back()
publishReaction({
event,
relays: [url],
content: emoji.unicode,
tags: [tagRoom(room, url)],
})
}
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
const showConversation = () => pushDrawer(ChannelThread, {url, room, event}, {replaceState: true})
const showInfo = () => pushModal(EventInfo, {event}, {replaceState: true})
const showDelete = () => pushModal(ConfirmDelete, {url, event})
</script>
<div class="col-2">
<Button class="btn btn-primary w-full" on:click={showEmojiPicker}>
<Icon size={4} icon="smile-circle" />
Send Reaction
</Button>
<Button class="btn btn-neutral w-full" on:click={showConversation}>
<Icon size={4} icon="reply" />
View Conversation
</Button>
<Button class="btn btn-neutral" on:click={showInfo}>
<Icon size={4} icon="code-2" />
Message Details
</Button>
{#if event.pubkey === $pubkey}
<Button class="btn btn-neutral text-error" on:click={showDelete}>
<Icon size={4} icon="trash-bin-2" />
Delete Message
</Button>
{/if}
</div>
@@ -1,6 +1,7 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import {makeReaction, sendWrapped} from "@app/commands"
@@ -11,4 +12,6 @@
sendWrapped({template: makeReaction({event, content: emoji.unicode}), pubkeys})
</script>
<EmojiButton {onEmoji} class="join-item" />
<EmojiButton {onEmoji} class="btn join-item btn-xs">
<Icon icon="smile-circle" size={4} />
</EmojiButton>
+19
View File
@@ -0,0 +1,19 @@
<script lang="ts">
import Confirm from "@lib/components/Confirm.svelte"
import {publishDelete} from "@app/commands"
export let url
export let event
const confirm = async () => {
await publishDelete({event, relays: [url]})
history.back()
}
</script>
<Confirm
{confirm}
title="Delete Message"
subtitle="Are you sure you want to delete this message?"
message="This will send a request to delete this message. Be aware that not all relays may honor this request." />
+19 -6
View File
@@ -2,11 +2,11 @@
import {nip19} from "nostr-tools"
import {nthEq} from "@welshman/lib"
import {Address} from "@welshman/util"
import {trackerStore} from "@welshman/app"
import type {TrustedEvent} from "@welshman/util"
import Link from "@lib/components/Link.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import NoteCard from "@app/components/NoteCard.svelte"
import {deriveEvent, entityLink, userMembership, getMembershipUrls, COMMENT} from "@app/state"
import {deriveEvent, entityLink, MESSAGE, THREAD} from "@app/state"
import {makeThreadPath} from "@app/routes"
export let value
@@ -18,13 +18,26 @@
const event = deriveEvent(idOrAddress, relays)
const entity = id ? nip19.neventEncode({id, relays}) : addr.toNaddr()
const getLocalHref = (e: TrustedEvent) => {
const url = e.tags.find(nthEq(0, "~"))?.[2]
console.log(e, url)
if (!url) return
if ([MESSAGE, THREAD].includes(e.kind)) return makeThreadPath(url, e.id)
const kind = e.tags.find(nthEq(0, "K"))?.[1]
const id = e.tags.find(nthEq(0, "E"))?.[1]
if (!id || !kind) return
if ([MESSAGE, THREAD].includes(parseInt(kind))) return makeThreadPath(url, id)
}
// If we found this event on a relay that the user is a member of, redirect internally
$: url = getMembershipUrls($userMembership).find(url => $trackerStore.hasRelay($event?.id, url))
$: root = $event?.kind === COMMENT ? $event.tags.find(nthEq(0, "E"))?.[1] : $event?.id
$: href = url && root ? makeThreadPath(url, root) : entityLink(entity)
$: localHref = getLocalHref($event)
$: href = localHref || entityLink(entity)
</script>
<Link external={!url} {href} class="my-2 block max-w-full text-left">
<Link external={!localHref} {href} class="my-2 block max-w-full text-left">
{#if $event}
<NoteCard event={$event} class="bg-alt rounded-box p-4">
<slot name="note-content" event={$event} {depth} />
+2 -2
View File
@@ -41,7 +41,7 @@
pushModal(
ProfileList,
{pubkeys: members, title: `Members of`, subtitle: displayRelayUrl(url)},
{replaceState}
{replaceState},
)
const createInvite = () => pushModal(SpaceInvite, {url}, {replaceState})
@@ -72,7 +72,7 @@
$: members = $memberships.filter(l => hasMembershipUrl(l, url)).map(l => l.event.pubkey)
onMount(async () => {
replaceState = Boolean(element.closest('.drawer'))
replaceState = Boolean(element.closest(".drawer"))
const error = (await checkRelayConnection(url)) || (await checkRelayAuth(url))
+1 -1
View File
@@ -25,7 +25,7 @@
class:border={isOwn}
class:border-solid={isOwn}
class:border-primary={isOwn}
on:click={onClick}>
on:click|stopPropagation={onClick}>
<span>{displayReaction(content)}</span>
{#if events.length > 1}
<span>{events.length}</span>
+3 -1
View File
@@ -62,7 +62,9 @@
</div>
{/if}
<Button class="join rounded-full">
<EmojiButton {onEmoji} class="join-item btn-neutral" />
<EmojiButton {onEmoji} class="btn join-item btn-neutral btn-xs">
<Icon icon="smile-circle" size={4} />
</EmojiButton>
<Tippy
bind:popover
component={ThreadMenu}
+6 -4
View File
@@ -3,14 +3,16 @@
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {append} from "@welshman/lib"
import {createEvent} from "@welshman/util"
import {publishThunk} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ModalHeader from "@lib/components/ModalHeader.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {pushToast} from '@app/toast'
import {THREAD} from "@app/state"
import {pushToast} from "@app/toast"
import {THREAD, GENERAL, tagRoom} from "@app/state"
import {getPubkeyHints} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
@@ -24,11 +26,11 @@
const submit = () => {
const content = $editor.getText()
const tags = getEditorTags($editor)
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
if (!content.trim()) {
return pushToast({
theme: 'error',
theme: "error",
message: "Please provide a message for your thread.",
})
}
+2 -14
View File
@@ -2,10 +2,9 @@
import {pubkey} from "@welshman/app"
import Button from "@lib/components/Button.svelte"
import Icon from "@lib/components/Icon.svelte"
import Confirm from "@lib/components/Confirm.svelte"
import EventInfo from "@app/components/EventInfo.svelte"
import ThreadShare from "@app/components/ThreadShare.svelte"
import {publishDelete} from "@app/commands"
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
import {pushModal} from "@app/modal"
import {COMMENT} from "@app/state"
@@ -27,18 +26,7 @@
const showDelete = () => {
onClick()
pushModal(Confirm, {
title: "Delete Message",
subtitle: "Are you sure you want to delete this message?",
message: `
This will send a request to delete this message.
Be aware that not all relays may honor this request.`,
confirm: async () => {
await publishDelete({event, relays: [url]})
history.back()
},
})
pushModal(ConfirmDelete, {url, event})
}
</script>
+6 -4
View File
@@ -3,13 +3,15 @@
import type {Readable} from "svelte/store"
import {writable} from "svelte/store"
import {createEditor, type Editor, EditorContent} from "svelte-tiptap"
import {append} from "@welshman/lib"
import {fly, slideAndFade} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ModalFooter from "@lib/components/ModalFooter.svelte"
import {getPubkeyHints, publishComment} from "@app/commands"
import {getEditorOptions, addFile, uploadFiles, getEditorTags} from "@lib/editor"
import {pushToast} from '@app/toast'
import {getPubkeyHints, publishComment} from "@app/commands"
import {tagRoom, GENERAL} from "@app/state"
import {pushToast} from "@app/toast"
export let url
export let event
@@ -22,11 +24,11 @@
const submit = () => {
const content = $editor.getText()
const tags = getEditorTags($editor)
const tags = append(tagRoom(GENERAL, url), getEditorTags($editor))
if (!content.trim()) {
return pushToast({
theme: 'error',
theme: "error",
message: "Please provide a message for your reply.",
})
}
+1 -1
View File
@@ -20,7 +20,7 @@
const back = () => history.back()
const onSubmit = () => {
setKey('content', toNostrURI(nevent))
setKey("content", toNostrURI(nevent))
goto(makeRoomPath(url, selection), {replaceState: true})
}
+4 -7
View File
@@ -34,7 +34,7 @@ import {
hasValidSignature,
normalizeRelayUrl,
} from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List} from "@welshman/util"
import type {TrustedEvent, SignedEvent, PublishedList, List, Filter} from "@welshman/util"
import {Nip59} from "@welshman/signer"
import {
pubkey,
@@ -45,7 +45,6 @@ import {
getDefaultAppContext,
getDefaultNetContext,
makeRouter,
trackerStore,
tracker,
relay,
getSession,
@@ -248,13 +247,11 @@ export const deriveEvent = (idOrAddress: string, hints: string[] = []) => {
)
}
export const deriveEventsForUrl = (url: string, kinds: number[]) =>
derived(trackerStore, $tracker =>
export const deriveEventsForUrl = (url: string, filters: Filter[]) =>
derived(deriveEvents(repository, {filters}), $events =>
sortBy(
e => -e.created_at,
Array.from($tracker.getIds(url))
.map(id => repository.eventsById.get(id)!)
.filter(e => kinds.includes(e?.kind)),
$events.filter(e => e.tags.find(nthEq(0, "~"))?.[2] === url),
),
)
+1 -1
View File
@@ -4,7 +4,7 @@
export let onClose
</script>
<div class="fixed inset-0 z-modal drawer">
<div class="drawer fixed inset-0 z-modal">
<button
class="absolute inset-0 cursor-pointer bg-black opacity-50"
transition:fade
+2 -3
View File
@@ -3,7 +3,6 @@
import {type Instance} from "tippy.js"
import type {NativeEmoji} from "emoji-picker-element/shared"
import {between} from "@welshman/lib"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import Tippy from "@lib/components/Tippy.svelte"
import EmojiPicker from "@lib/components/EmojiPicker.svelte"
@@ -37,7 +36,7 @@
component={EmojiPicker}
props={{onClick}}
params={{trigger: "manual", interactive: true}}>
<Button class="btn btn-xs {$$props.class}" on:click={open}>
<Icon icon="smile-circle" size={4} />
<Button on:click={open} class={$$props.class}>
<slot />
</Button>
</Tippy>
+10
View File
@@ -1,3 +1,13 @@
<style>
@media (max-width: 450px) {
emoji-picker {
max-width: 100%;
--num-columns: 6;
--category-emoji-size: 1.125rem;
}
}
</style>
<script lang="ts">
import "emoji-picker-element"
import type {Emoji} from "emoji-picker-element/shared"
+2
View File
@@ -68,3 +68,5 @@ export const createScroller = ({
},
}
}
export const isMobile = "ontouchstart" in document.documentElement
+7 -2
View File
@@ -31,10 +31,15 @@
<Page>
<ContentSearch>
<label slot="input" class="input input-bordered row-2">
<label slot="input" class="row-2 input input-bordered">
<Icon icon="magnifer" />
<!-- svelte-ignore a11y-autofocus -->
<input autofocus bind:value={term} class="grow" type="text" placeholder="Search for people..." />
<input
autofocus
bind:value={term}
class="grow"
type="text"
placeholder="Search for people..." />
</label>
<div slot="content" class="col-2" bind:this={element}>
{#each pubkeys.slice(0, limit) as pubkey (pubkey)}
+2 -2
View File
@@ -18,9 +18,9 @@
import {pushModal, pushDrawer} from "@app/modal"
const url = decodeRelay($page.params.relay)
const events = deriveEventsForUrl(url, [THREAD])
const events = deriveEventsForUrl(url, [{kinds: [THREAD]}])
const mutedPubkeys = getPubkeyTagValues(getListTags($userMutes))
const filters: Filter[] = [{kinds: [THREAD]}, {kinds: [COMMENT], '#k': [String(THREAD)]}]
const filters: Filter[] = [{kinds: [THREAD]}, {kinds: [COMMENT], "#k": [String(THREAD)]}]
const feed = makeIntersectionFeed(makeRelayFeed(url), feedsFromFilters(filters))
const loader = feedLoader.getLoader(feed, {
onExhausted: () => {
@@ -11,7 +11,7 @@
import {onMount} from "svelte"
import {page} from "$app/stores"
import {writable} from "svelte/store"
import {sortBy, fromPairs, now, assoc, append} from "@welshman/lib"
import {sortBy, now, assoc, append} from "@welshman/lib"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {createEvent} from "@welshman/util"
import {formatTimestampAsDate, subscribe, publishThunk} from "@welshman/app"
@@ -41,7 +41,7 @@
import {popKey} from "@app/implicit"
const {room = GENERAL} = $page.params
const content = popKey<string>('content') || ""
const content = popKey<string>("content") || ""
const url = decodeRelay($page.params.relay)
const channel = deriveChannel(makeChannelId(url, room))
const thunks = writable({} as Record<string, Thunk>)
@@ -18,7 +18,7 @@
const url = decodeRelay($page.params.relay)
const kinds = [EVENT_DATE, EVENT_TIME]
const events = deriveEventsForUrl(url, kinds)
const events = deriveEventsForUrl(url, [{kinds}])
const openMenu = () => pushDrawer(MenuSpace, {url})
@@ -1,11 +1,10 @@
<script lang="ts">
import {onMount} from "svelte"
import {sortBy, displayUrl, sleep} from "@welshman/lib"
import {sortBy, sleep} from "@welshman/lib"
import {page} from "$app/stores"
import {repository, subscribe} from "@welshman/app"
import {deriveEvents} from "@welshman/store"
import Icon from "@lib/components/Icon.svelte"
import Link from "@lib/components/Link.svelte"
import PageBar from "@lib/components/PageBar.svelte"
import Spinner from "@lib/components/Spinner.svelte"
import Button from "@lib/components/Button.svelte"
@@ -16,7 +15,6 @@
import ThreadReply from "@app/components/ThreadReply.svelte"
import {COMMENT, deriveEvent, decodeRelay} from "@app/state"
import {pushDrawer} from "@app/modal"
import {makeSpacePath} from "@app/routes"
const {relay, id} = $page.params
const url = decodeRelay(relay)