mirror of
https://github.com/coracle-social/flotilla.git
synced 2025-12-11 11:27:03 +00:00
Apply layout changes to chat
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user