Apply layout changes to chat

This commit is contained in:
Jon Staab
2025-03-04 10:20:06 -08:00
parent 1c0b2a09df
commit a582b1ea73
5 changed files with 177 additions and 140 deletions

View File

@@ -10,6 +10,7 @@
* Fix nevent hints for url-specific stuff * Fix nevent hints for url-specific stuff
* Add alerts via Anchor * Add alerts via Anchor
* Fix confirm and reactions on mobile * Fix confirm and reactions on mobile
* Add reply to chat on mobile
# 0.2.11 # 0.2.11

View File

@@ -99,15 +99,17 @@
<div class="row-2 ml-10 mt-1"> <div class="row-2 ml-10 mt-1">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" /> <ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
</div> </div>
<button {#if !isMobile}
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all" <button
class:group-hover:opacity-100={!isMobile}> class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
<ChannelMessageEmojiButton {url} {room} {event} /> class:group-hover:opacity-100={!isMobile}>
{#if replyTo} <ChannelMessageEmojiButton {url} {room} {event} />
<Button class="btn join-item btn-xs" onclick={reply}> {#if replyTo}
<Icon icon="reply" size={4} /> <Button class="btn join-item btn-xs" onclick={reply}>
</Button> <Icon icon="reply" size={4} />
{/if} </Button>
<ChannelMessageMenuButton {url} {event} /> {/if}
</button> <ChannelMessageMenuButton {url} {event} />
</button>
{/if}
</TapTarget> </TapTarget>

View File

@@ -70,6 +70,8 @@
let loading = $state(true) let loading = $state(true)
let compose: ChatCompose | undefined = $state() let compose: ChatCompose | undefined = $state()
let parent: TrustedEvent | undefined = $state() let parent: TrustedEvent | undefined = $state()
let parentPreview: HTMLElement | undefined = $state()
let dynamicPadding: HTMLElement | undefined = $state()
const elements = $derived.by(() => { const elements = $derived.by(() => {
const elements = [] const elements = []
@@ -104,6 +106,16 @@
onMount(() => { onMount(() => {
// Don't use loadInboxRelaySelection because we want to force reload // Don't use loadInboxRelaySelection because we want to force reload
load({filters: [{kinds: [INBOX_RELAYS], authors: others}]}) load({filters: [{kinds: [INBOX_RELAYS], authors: others}]})
const observer = new ResizeObserver(() => {
dynamicPadding!.style.minHeight = `${parentPreview!.offsetHeight}px`
})
observer.observe(parentPreview!)
return () => {
observer.unobserve(parentPreview!)
}
}) })
setTimeout(() => { setTimeout(() => {
@@ -111,110 +123,113 @@
}, 5000) }, 5000)
</script> </script>
<div class="relative flex h-full w-full flex-col"> {#if others.length > 0}
{#if others.length > 0} <PageBar class="chat__page-bar">
<PageBar> {#snippet title()}
{#snippet title()} <div class="flex flex-col gap-1 sm:flex-row sm:gap-2">
<div class="flex flex-col gap-1 sm:flex-row sm:gap-2"> {#if others.length === 1}
{#if others.length === 1} {@const pubkey = others[0]}
{@const pubkey = others[0]} {@const onClick = () => pushModal(ProfileDetail, {pubkey})}
{@const onClick = () => pushModal(ProfileDetail, {pubkey})} <Button onclick={onClick} class="row-2">
<Button onclick={onClick} class="row-2"> <ProfileCircle {pubkey} size={5} />
<ProfileCircle {pubkey} size={5} /> <ProfileName {pubkey} />
<ProfileName {pubkey} /> </Button>
</Button>
{:else}
<div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<ProfileName pubkey={others[0]} />
and
{#if others.length === 2}
<ProfileName pubkey={others[1]} />
{:else}
{others.length - 1}
{others.length > 2 ? "others" : "other"}
{/if}
</p>
</div>
{#if others.length > 2}
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{/if}
{/if}
</div>
{/snippet}
{#snippet action()}
<div>
{#if remove($pubkey, missingInboxes).length > 0}
{@const count = remove($pubkey, missingInboxes).length}
{@const label = count > 1 ? "inboxes are" : "inbox is"}
<div
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
data-tip="{count} {label} not configured.">
<Icon icon="danger" />
{count}
</div>
{/if}
</div>
{/snippet}
</PageBar>
{/if}
<div class="-mt-2 flex flex-grow flex-col-reverse overflow-auto py-2">
{#if missingInboxes.includes($pubkey!)}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon="danger" />
Your inbox is not configured.
</p>
<p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your
inbox.
</p>
</div>
</div>
{:else if missingInboxes.length > 0}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon="danger" />
{missingInboxes.length}
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
</p>
<p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
sure everyone in this conversation has set up their inbox relays.
</p>
</div>
</div>
{/if}
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"}
<Divider>{value}</Divider>
{:else}
<ChatMessage
event={$state.snapshot(value as TrustedEvent)}
{pubkeys}
{showPubkey}
{replyTo} />
{/if}
{/each}
<p
class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
<Spinner {loading}>
{#if loading}
Looking for messages...
{:else} {:else}
End of message history <div class="flex items-center gap-2">
<ProfileCircles pubkeys={others} size={5} />
<p class="overflow-hidden text-ellipsis whitespace-nowrap">
<ProfileName pubkey={others[0]} />
and
{#if others.length === 2}
<ProfileName pubkey={others[1]} />
{:else}
{others.length - 1}
{others.length > 2 ? "others" : "other"}
{/if}
</p>
</div>
{#if others.length > 2}
<Button onclick={showMembers} class="btn btn-link hidden sm:block"
>Show all members</Button>
{/if}
{/if} {/if}
</Spinner> </div>
{@render info?.()} {/snippet}
</p> {#snippet action()}
</div> <div>
{#if parent} {#if remove($pubkey, missingInboxes).length > 0}
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" /> {@const count = remove($pubkey, missingInboxes).length}
{@const label = count > 1 ? "inboxes are" : "inbox is"}
<div
class="row-2 badge badge-error badge-lg tooltip tooltip-left cursor-pointer"
data-tip="{count} {label} not configured.">
<Icon icon="danger" />
{count}
</div>
{/if}
</div>
{/snippet}
</PageBar>
{/if}
<div class="chat__messages scroll-container">
<div bind:this={dynamicPadding}></div>
{#if missingInboxes.includes($pubkey!)}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon="danger" />
Your inbox is not configured.
</p>
<p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please visit
your <Link class="link" href="/settings/relays">relay settings page</Link> to set up your inbox.
</p>
</div>
</div>
{:else if missingInboxes.length > 0}
<div class="py-12">
<div class="card2 col-2 m-auto max-w-md items-center text-center">
<p class="row-2 text-lg text-error">
<Icon icon="danger" />
{missingInboxes.length}
{missingInboxes.length > 1 ? "inboxes are" : "inbox is"} not configured.
</p>
<p>
In order to deliver messages, {PLATFORM_NAME} needs to know where to send them. Please make
sure everyone in this conversation has set up their inbox relays.
</p>
</div>
</div>
{/if} {/if}
{#each elements as { type, id, value, showPubkey } (id)}
{#if type === "date"}
<Divider>{value}</Divider>
{:else}
<ChatMessage
event={$state.snapshot(value as TrustedEvent)}
{pubkeys}
{showPubkey}
{replyTo} />
{/if}
{/each}
<p class="m-auto flex h-10 max-w-sm flex-col items-center justify-center gap-4 py-20 text-center">
<Spinner {loading}>
{#if loading}
Looking for messages...
{:else}
End of message history
{/if}
</Spinner>
{@render info?.()}
</p>
</div>
<div class="chat__compose bg-base-200">
<div bind:this={parentPreview}>
{#if parent}
<ChatComposeParent event={parent} clear={clearParent} verb="Replying to" />
{/if}
</div>
<ChatCompose bind:this={compose} {onSubmit} /> <ChatCompose bind:this={compose} {onSubmit} />
</div> </div>

View File

@@ -27,12 +27,12 @@
interface Props { interface Props {
event: TrustedEvent event: TrustedEvent
replyTo?: any replyTo: (event: TrustedEvent) => void
pubkeys: string[] pubkeys: string[]
showPubkey?: boolean showPubkey?: boolean
} }
const {event, replyTo = undefined, pubkeys, showPubkey = false}: Props = $props() const {event, replyTo, pubkeys, showPubkey = false}: Props = $props()
const thunk = $thunks[event.id] const thunk = $thunks[event.id]
const isOwn = event.pubkey === $pubkey const isOwn = event.pubkey === $pubkey
@@ -40,6 +40,8 @@
const profileDisplay = deriveProfileDisplay(event.pubkey) const profileDisplay = deriveProfileDisplay(event.pubkey)
const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length] const [_, colorValue] = colors[parseInt(hash(event.pubkey)) % colors.length]
const reply = () => replyTo(event)
const onReactionClick = async (content: string, events: TrustedEvent[]) => { const onReactionClick = async (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey) const reaction = events.find(e => e.pubkey === $pubkey)
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content}) const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
@@ -49,7 +51,7 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey}) const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys}) const showMobileMenu = () => pushModal(ChatMessageMenuMobile, {event, pubkeys, reply})
const togglePopover = () => { const togglePopover = () => {
if (popoverIsVisible) { if (popoverIsVisible) {
@@ -72,31 +74,33 @@
class:chat-start={!isOwn} class:chat-start={!isOwn}
class:flex-row-reverse={!isOwn} class:flex-row-reverse={!isOwn}
class:chat-end={isOwn}> class:chat-end={isOwn}>
<Tippy {#if !isMobile}
bind:popover <Tippy
component={ChatMessageMenu} bind:popover
props={{event, pubkeys, popover, replyTo}} component={ChatMessageMenu}
params={{ props={{event, pubkeys, popover, replyTo}}
interactive: true, params={{
trigger: "manual", interactive: true,
onShow() { trigger: "manual",
popoverIsVisible = true onShow() {
}, popoverIsVisible = true
onHidden() { },
popoverIsVisible = false onHidden() {
}, popoverIsVisible = false
}}> },
<button }}>
type="button" <button
class="opacity-0 transition-all" type="button"
class:group-hover:opacity-100={!isMobile} class="opacity-0 transition-all"
onclick={togglePopover}> class:group-hover:opacity-100={!isMobile}
<Icon icon="menu-dots" size={4} /> onclick={togglePopover}>
</button> <Icon icon="menu-dots" size={4} />
</Tippy> </button>
</Tippy>
{/if}
<div class="flex min-w-0 flex-col" class:items-end={isOwn}> <div class="flex min-w-0 flex-col" class:items-end={isOwn}>
<TapTarget <TapTarget
class="bg-alt chat-bubble mx-1 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl" class="bg-alt chat-bubble mx-1 mb-2 flex cursor-auto flex-col gap-1 text-left lg:max-w-2xl"
onTap={showMobileMenu}> onTap={showMobileMenu}>
{#if showPubkey} {#if showPubkey}
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">

View File

@@ -9,7 +9,13 @@
import {pushModal} from "@app/modal" import {pushModal} from "@app/modal"
import {clip} from "@app/toast" import {clip} from "@app/toast"
const {event, pubkeys} = $props() type Props = {
pubkeys: string[]
event: TrustedEvent
reply: () => void
}
const {event, pubkeys, reply}: Props = $props()
const onEmoji = ((event: TrustedEvent, emoji: NativeEmoji) => { const onEmoji = ((event: TrustedEvent, emoji: NativeEmoji) => {
history.back() history.back()
@@ -18,6 +24,11 @@
const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true}) const showEmojiPicker = () => pushModal(EmojiPicker, {onClick: onEmoji}, {replaceState: true})
const sendReply = () => {
history.back()
reply()
}
const copyText = () => { const copyText = () => {
history.back() history.back()
clip(event.content) clip(event.content)
@@ -31,6 +42,10 @@
<Icon size={4} icon="smile-circle" /> <Icon size={4} icon="smile-circle" />
Send Reaction Send Reaction
</Button> </Button>
<Button class="btn btn-neutral w-full" onclick={sendReply}>
<Icon size={4} icon="reply" />
Send Reply
</Button>
<Button class="btn btn-neutral w-full" onclick={copyText}> <Button class="btn btn-neutral w-full" onclick={copyText}>
<Icon size={4} icon="copy" /> <Icon size={4} icon="copy" />
Copy Text Copy Text