Add reports to channel messages
This commit is contained in:
@@ -2,6 +2,7 @@ import {get} from "svelte/store"
|
|||||||
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
import {ctx, sample, uniq, sleep, chunk, equals} from "@welshman/lib"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
|
REPORT,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
INBOX_RELAYS,
|
INBOX_RELAYS,
|
||||||
RELAYS,
|
RELAYS,
|
||||||
@@ -429,6 +430,29 @@ export const makeDelete = ({event}: {event: TrustedEvent}) => {
|
|||||||
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
export const publishDelete = ({relays, event}: {relays: string[]; event: TrustedEvent}) =>
|
||||||
publishThunk({event: makeDelete({event}), relays})
|
publishThunk({event: makeDelete({event}), relays})
|
||||||
|
|
||||||
|
export type ReportParams = {
|
||||||
|
event: TrustedEvent
|
||||||
|
content: string
|
||||||
|
reason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const makeReport = ({event, reason, content}: ReportParams) => {
|
||||||
|
const tags = [
|
||||||
|
["p", event.pubkey],
|
||||||
|
["e", event.id, reason],
|
||||||
|
]
|
||||||
|
|
||||||
|
return createEvent(REPORT, {content, tags})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const publishReport = ({
|
||||||
|
relays,
|
||||||
|
event,
|
||||||
|
reason,
|
||||||
|
content,
|
||||||
|
}: ReportParams & {relays: string[]}) =>
|
||||||
|
publishThunk({event: makeReport({event, reason, content}), relays})
|
||||||
|
|
||||||
export type ReactionParams = {
|
export type ReactionParams = {
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
content: string
|
content: string
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row-2 ml-10 mt-1">
|
<div class="row-2 ml-10 mt-1">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right" />
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
class="join absolute right-1 top-1 border border-solid border-neutral text-xs opacity-0 transition-all"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
@@ -10,6 +11,11 @@
|
|||||||
export let event
|
export let event
|
||||||
export let onClick
|
export let onClick
|
||||||
|
|
||||||
|
const report = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(EventReport, {url, event})
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {event})
|
||||||
@@ -35,5 +41,12 @@
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" on:click={report}>
|
||||||
|
<Icon size={4} icon="danger" />
|
||||||
|
Report Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Field from "@lib/components/Field.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import {pushToast} from "@app/toast"
|
||||||
|
import {publishReport} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const confirm = async () => {
|
||||||
|
if (!reason) {
|
||||||
|
return pushToast({
|
||||||
|
theme: "error",
|
||||||
|
message: "Please select a reason for your report.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
await publishReport({event, reason: reason.toLowerCase(), content, relays: [url]})
|
||||||
|
|
||||||
|
loading = false
|
||||||
|
history.back()
|
||||||
|
|
||||||
|
return pushToast({message: "Your report has been sent!"})
|
||||||
|
}
|
||||||
|
|
||||||
|
let reason = ""
|
||||||
|
let content = ""
|
||||||
|
let loading = false
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form class="column gap-4" on:submit|preventDefault={confirm}>
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Report Content</div>
|
||||||
|
<div slot="info">Flag inappropriate content.</div>
|
||||||
|
</ModalHeader>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Reason*</p>
|
||||||
|
<select slot="input" class="select select-bordered" bind:value={reason}>
|
||||||
|
<option disabled selected>Choose a reason</option>
|
||||||
|
<option>Nudity</option>
|
||||||
|
<option>Malware</option>
|
||||||
|
<option>Profanity</option>
|
||||||
|
<option>Illegal</option>
|
||||||
|
<option>Spam</option>
|
||||||
|
<option>Impersonation</option>
|
||||||
|
<option>Other</option>
|
||||||
|
</select>
|
||||||
|
<p slot="info">Please select a reason for your report.</p>
|
||||||
|
</Field>
|
||||||
|
<Field>
|
||||||
|
<p slot="label">Details</p>
|
||||||
|
<textarea slot="input" class="textarea textarea-bordered" bind:value={content} />
|
||||||
|
<p slot="info">Please provide any additional details relevant to your report.</p>
|
||||||
|
</Field>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" on:click={back}>
|
||||||
|
<Icon icon="alt-arrow-left" />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
<Button type="submit" class="btn btn-primary" disabled={loading}>
|
||||||
|
<Spinner {loading}>Send Report</Spinner>
|
||||||
|
<Icon icon="alt-arrow-right" />
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</form>
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import {getTag, REPORT} from "@welshman/util"
|
||||||
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
|
import {deriveEvents} from "@welshman/store"
|
||||||
|
import {pubkey, repository} from "@welshman/app"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Profile from "@app/components/Profile.svelte"
|
||||||
|
import {publishDelete} from "@app/commands"
|
||||||
|
|
||||||
|
export let url
|
||||||
|
export let event
|
||||||
|
|
||||||
|
const reports = deriveEvents(repository, {
|
||||||
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const deleteReport = (report: TrustedEvent) => {
|
||||||
|
publishDelete({event: report, relays: [url]})
|
||||||
|
|
||||||
|
if ($reports.length === 0) {
|
||||||
|
history.back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getReason = (tags: string[][]) => getTag("e", tags)?.[2] || "other"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4">
|
||||||
|
<ModalHeader>
|
||||||
|
<div slot="title">Report Details</div>
|
||||||
|
<div slot="info">All reports for this event are shown below.</div>
|
||||||
|
</ModalHeader>
|
||||||
|
{#each $reports as report (report.id)}
|
||||||
|
{@const reason = getReason(report.tags)}
|
||||||
|
{@const remove = () => deleteReport(report)}
|
||||||
|
<div class="column gap-2">
|
||||||
|
<div class="flex justify-between">
|
||||||
|
<div>
|
||||||
|
<Profile pubkey={report.pubkey} />
|
||||||
|
<span>Reported this event as "{reason}"</span>
|
||||||
|
</div>
|
||||||
|
{#if report.pubkey === $pubkey}
|
||||||
|
<Button class="btn-default btn" on:click={remove}>Delete Report</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{#if report.content}
|
||||||
|
<p>"{report.content}"</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
<Button class="btn btn-primary" on:click={back}>Got it</Button>
|
||||||
|
</div>
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<NoteCard {event} class="card2 bg-alt">
|
<NoteCard {event} class="card2 bg-alt">
|
||||||
<Content {event} expandMode="inline" />
|
<Content {event} expandMode="inline" />
|
||||||
<div class="flex w-full justify-between gap-2">
|
<div class="flex w-full justify-between gap-2">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-right">
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
|
||||||
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
|
||||||
<Icon icon="smile-circle" size={4} />
|
<Icon icon="smile-circle" size={4} />
|
||||||
</EmojiButton>
|
</EmojiButton>
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {groupBy, uniqBy, batch} from "@welshman/lib"
|
import {groupBy, uniq, uniqBy, batch} from "@welshman/lib"
|
||||||
import {REACTION, DELETE} from "@welshman/util"
|
import {REACTION, getTag, REPORT, DELETE} from "@welshman/util"
|
||||||
import type {TrustedEvent} from "@welshman/util"
|
import type {TrustedEvent} from "@welshman/util"
|
||||||
import {deriveEvents} from "@welshman/store"
|
import {deriveEvents} from "@welshman/store"
|
||||||
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
import {pubkey, repository, load, displayProfileByPubkey} from "@welshman/app"
|
||||||
import {displayList} from "@lib/util"
|
import {displayList} from "@lib/util"
|
||||||
import {isMobile} from "@lib/html"
|
import {isMobile} from "@lib/html"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import EventReportDetails from "@app/components/EventReportDetails.svelte"
|
||||||
import {displayReaction} from "@app/state"
|
import {displayReaction} from "@app/state"
|
||||||
|
import {pushModal} from "@app/modal"
|
||||||
|
|
||||||
export let event
|
export let event
|
||||||
export let onReactionClick
|
export let onReactionClick
|
||||||
export let relays: string[] = []
|
export let url = ""
|
||||||
export let reactionClass = ""
|
export let reactionClass = ""
|
||||||
export let noTooltip = false
|
export let noTooltip = false
|
||||||
|
|
||||||
|
const reports = deriveEvents(repository, {
|
||||||
|
filters: [{kinds: [REPORT], "#e": [event.id]}],
|
||||||
|
})
|
||||||
|
|
||||||
const reactions = deriveEvents(repository, {
|
const reactions = deriveEvents(repository, {
|
||||||
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
filters: [{kinds: [REACTION], "#e": [event.id]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const onReportClick = () => pushModal(EventReportDetails, {url, event})
|
||||||
|
|
||||||
|
$: reportReasons = uniq($reports.map(e => getTag("e", e.tags)?.[2]))
|
||||||
|
|
||||||
$: groupedReactions = groupBy(
|
$: groupedReactions = groupBy(
|
||||||
e => e.content,
|
e => e.content,
|
||||||
uniqBy(e => e.pubkey + e.content, $reactions),
|
uniqBy(e => e.pubkey + e.content, $reactions),
|
||||||
@@ -26,11 +37,11 @@
|
|||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
load({
|
load({
|
||||||
relays,
|
relays: [url],
|
||||||
filters: [{kinds: [REACTION, DELETE], "#e": [event.id]}],
|
filters: [{kinds: [REACTION, REPORT, DELETE], "#e": [event.id]}],
|
||||||
onEvent: batch(300, (events: TrustedEvent[]) => {
|
onEvent: batch(300, (events: TrustedEvent[]) => {
|
||||||
load({
|
load({
|
||||||
relays,
|
relays: [url],
|
||||||
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
|
filters: [{kinds: [DELETE], "#e": events.map(e => e.id)}],
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
@@ -38,8 +49,19 @@
|
|||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $reactions.length > 0}
|
{#if $reactions.length > 0 || $reports.length > 0}
|
||||||
<div class="flex min-w-0 flex-wrap gap-2">
|
<div class="flex min-w-0 flex-wrap gap-2">
|
||||||
|
{#if url && $reports.length > 0}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-tip={`This content has been reported as "${displayList(reportReasons)}".`}}
|
||||||
|
class="btn btn-error btn-xs tooltip-right flex items-center gap-1 rounded-full"
|
||||||
|
class:tooltip={!noTooltip && !isMobile}
|
||||||
|
on:click|preventDefault|stopPropagation={onReportClick}>
|
||||||
|
<Icon icon="danger" />
|
||||||
|
<span>{$reports.length}</span>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
{#each groupedReactions.entries() as [content, events]}
|
{#each groupedReactions.entries() as [content, events]}
|
||||||
{@const pubkeys = events.map(e => e.pubkey)}
|
{@const pubkeys = events.map(e => e.pubkey)}
|
||||||
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||||
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
<div class="flex flex-grow flex-wrap justify-end gap-2">
|
||||||
<ReactionSummary relays={[url]} {event} {onReactionClick} reactionClass="tooltip-left" />
|
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
|
||||||
{#if $deleted}
|
{#if $deleted}
|
||||||
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
<div class="btn btn-error btn-xs rounded-full">Deleted</div>
|
||||||
{:else if thunk}
|
{:else if thunk}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
import Icon from "@lib/components/Icon.svelte"
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
|
import EventReport from "@app/components/EventReport.svelte"
|
||||||
import ThreadShare from "@app/components/ThreadShare.svelte"
|
import ThreadShare from "@app/components/ThreadShare.svelte"
|
||||||
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
import ConfirmDelete from "@app/components/ConfirmDelete.svelte"
|
||||||
import {pushModal} from "@app/modal"
|
import {pushModal} from "@app/modal"
|
||||||
@@ -14,6 +15,11 @@
|
|||||||
|
|
||||||
const isRoot = event.kind !== COMMENT
|
const isRoot = event.kind !== COMMENT
|
||||||
|
|
||||||
|
const report = () => {
|
||||||
|
onClick()
|
||||||
|
pushModal(EventReport, {url, event})
|
||||||
|
}
|
||||||
|
|
||||||
const showInfo = () => {
|
const showInfo = () => {
|
||||||
onClick()
|
onClick()
|
||||||
pushModal(EventInfo, {event})
|
pushModal(EventInfo, {event})
|
||||||
@@ -52,5 +58,12 @@
|
|||||||
Delete Message
|
Delete Message
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
|
{:else}
|
||||||
|
<li>
|
||||||
|
<Button class="text-error" on:click={report}>
|
||||||
|
<Icon size={4} icon="danger" />
|
||||||
|
Report Content
|
||||||
|
</Button>
|
||||||
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -42,9 +42,9 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
{#if isFailure && failure}
|
||||||
{#if isFailure && failure}
|
{@const [url, {message, status}] = failure}
|
||||||
{@const [url, {message, status}] = failure}
|
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||||
<Tippy
|
<Tippy
|
||||||
class="flex items-center {$$props.class}"
|
class="flex items-center {$$props.class}"
|
||||||
component={ThunkStatusDetail}
|
component={ThunkStatusDetail}
|
||||||
@@ -55,7 +55,9 @@
|
|||||||
<span>Failed to send!</span>
|
<span>Failed to send!</span>
|
||||||
</span>
|
</span>
|
||||||
</Tippy>
|
</Tippy>
|
||||||
{:else if canCancel || isPending}
|
</div>
|
||||||
|
{:else if canCancel || isPending}
|
||||||
|
<div class="flex justify-end px-1 text-xs {$$props.class}">
|
||||||
<span class="flex items-center gap-1 {$$props.class}">
|
<span class="flex items-center gap-1 {$$props.class}">
|
||||||
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px" />
|
||||||
<span class="opacity-50">Sending...</span>
|
<span class="opacity-50">Sending...</span>
|
||||||
@@ -63,5 +65,5 @@
|
|||||||
<Button class="link" on:click={abort}>Cancel</Button>
|
<Button class="link" on:click={abort}>Cancel</Button>
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|||||||
Reference in New Issue
Block a user