Migrate Reports dialog to ActionItems dialog, add room join requests to queue
This commit is contained in:
parent
0761cdd28f
commit
1f5be54cb1
15 changed files with 326 additions and 115 deletions
|
|
@ -169,6 +169,7 @@ src/
|
||||||
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
- When creating forms, use `FieldInline` or `Field` instead of custom elements/tailwindcss
|
||||||
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
- Do not define svelte event handlers inline, instead name them and put them in the script section of templates
|
||||||
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
- Avoid using `as`, except where necessary. Instead, annotate function parameters, and ensure upstream values are typed correctly.
|
||||||
|
- Instead of `getTag(tagName, event.tags)?.[1] || ""`, use `getTagValue(tagName, event.tags)`
|
||||||
|
|
||||||
**Human-First Simplicity (Jon Staab Style):**
|
**Human-First Simplicity (Jon Staab Style):**
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@
|
||||||
import EventInfo from "@app/components/EventInfo.svelte"
|
import EventInfo from "@app/components/EventInfo.svelte"
|
||||||
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
import ProfileBadges from "@app/components/ProfileBadges.svelte"
|
||||||
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
import {pubkeyLink, deriveUserIsSpaceAdmin, deriveSpaceBannedPubkeyItems} from "@app/core/state"
|
||||||
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {makeChatPath} from "@app/util/routes"
|
import {makeChatPath} from "@app/util/routes"
|
||||||
|
|
@ -85,10 +86,7 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
const restoreMember = async () => {
|
const restoreMember = async () => {
|
||||||
const {error} = await manageRelay(url!, {
|
const error = await addSpaceMembers(url!, [pubkey])
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,10 @@
|
||||||
type Props = {
|
type Props = {
|
||||||
url: string
|
url: string
|
||||||
event: TrustedEvent
|
event: TrustedEvent
|
||||||
onDelete?: () => void
|
onResolved?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const {url, event, onDelete}: Props = $props()
|
const {url, event, onResolved}: Props = $props()
|
||||||
|
|
||||||
const etag = getTag("e", event.tags)
|
const etag = getTag("e", event.tags)
|
||||||
const ptag = getTag("p", event.tags)
|
const ptag = getTag("p", event.tags)
|
||||||
|
|
@ -45,7 +45,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<ReportMenu {url} {event} {onDelete} />
|
<ReportMenu {url} {event} {onResolved} />
|
||||||
</div>
|
</div>
|
||||||
{#if event.content}
|
{#if event.content}
|
||||||
<div class="border-l-2 border-primary pl-3">
|
<div class="border-l-2 border-primary pl-3">
|
||||||
|
|
|
||||||
|
|
@ -208,7 +208,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
<input type="checkbox" class="checkbox" bind:checked={values.isClosed} />
|
||||||
<span class="text-sm opacity-75">Ignore requests to join</span>
|
<span class="text-sm opacity-75">Membership requires approval</span>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
{@render footer({loading})}
|
{@render footer({loading})}
|
||||||
|
|
|
||||||
83
src/app/components/RoomJoinItem.svelte
Normal file
83
src/app/components/RoomJoinItem.svelte
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import {getTagValue} from "@welshman/util"
|
||||||
|
import type {TrustedEvent, PublishedRoomMeta} from "@welshman/util"
|
||||||
|
import {repository} from "@welshman/app"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import ProfileName from "@app/components/ProfileName.svelte"
|
||||||
|
import ProfileDetail from "@app/components/ProfileDetail.svelte"
|
||||||
|
import RoomName from "@app/components/RoomName.svelte"
|
||||||
|
import {pushModal} from "@app/util/modal"
|
||||||
|
import {pushToast} from "@app/util/toast"
|
||||||
|
import {deriveRoom} from "@app/core/state"
|
||||||
|
import {addRoomMembers} from "@app/core/commands"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
url: string
|
||||||
|
event: TrustedEvent
|
||||||
|
onResolved?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url, event, onResolved}: Props = $props()
|
||||||
|
|
||||||
|
const h = getTagValue("h", event.tags) || ""
|
||||||
|
const room = deriveRoom(url, h)
|
||||||
|
|
||||||
|
const showProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
|
||||||
|
|
||||||
|
const dismiss = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {error} = await manageRelay(url, {
|
||||||
|
method: ManagementMethod.BanEvent,
|
||||||
|
params: [event.id, "Join request dismissed"]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Join request has been dismissed."})
|
||||||
|
repository.removeEvent(event.id)
|
||||||
|
onResolved?.()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const accept = async () => {
|
||||||
|
loading = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const error = await addRoomMembers(url, $room as PublishedRoomMeta, [event.pubkey])
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
pushToast({theme: "error", message: error})
|
||||||
|
} else {
|
||||||
|
pushToast({message: "Member has been added to the room!"})
|
||||||
|
onResolved?.()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let loading = $state(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="column gap-4 card2 card2-sm bg-alt">
|
||||||
|
<div class="flex justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<Button class="inline text-primary" onclick={showProfile}>
|
||||||
|
<ProfileName pubkey={event.pubkey} {url} />
|
||||||
|
</Button>
|
||||||
|
<span>
|
||||||
|
requested membership in #<RoomName {url} {h} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button class="btn btn-neutral btn-sm" onclick={dismiss} disabled={loading}>Dismiss</Button>
|
||||||
|
<Button class="btn btn-primary btn-sm" onclick={accept} disabled={loading}>Accept</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {setKey, popKey} from "@lib/implicit"
|
import {setKey, popKey} from "@lib/implicit"
|
||||||
import {sleep} from "@welshman/lib"
|
import {sleep} from "@welshman/lib"
|
||||||
import {ManagementMethod} from "@welshman/util"
|
import {displayProfileByPubkey} from "@welshman/app"
|
||||||
import {manageRelay} from "@welshman/app"
|
import type {PublishedRoomMeta} from "@welshman/util"
|
||||||
import {addRoomMember, displayProfileByPubkey, waitForThunkError} from "@welshman/app"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
@ -22,6 +21,7 @@
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
import {pushModal} from "@app/util/modal"
|
import {pushModal} from "@app/util/modal"
|
||||||
import {deriveRoom, deriveSpaceMembers} from "@app/core/state"
|
import {deriveRoom, deriveSpaceMembers} from "@app/core/state"
|
||||||
|
import {addRoomMembers} from "@app/core/commands"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
url: string
|
url: string
|
||||||
|
|
@ -42,35 +42,14 @@
|
||||||
// Show loading for auto submit callback
|
// Show loading for auto submit callback
|
||||||
await sleep(500)
|
await sleep(500)
|
||||||
|
|
||||||
const results = await Promise.all(
|
const error = await addRoomMembers(url, $room as PublishedRoomMeta, pubkeys)
|
||||||
pubkeys
|
|
||||||
.filter(pubkey => !$spaceMembers.includes(pubkey))
|
|
||||||
.map(pubkey =>
|
|
||||||
manageRelay(url, {
|
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const {error} of results) {
|
if (error) {
|
||||||
if (error) {
|
pushToast({theme: "error", message: error})
|
||||||
return pushToast({theme: "error", message: error})
|
} else {
|
||||||
}
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
}
|
}
|
||||||
|
|
||||||
const errors = await Promise.all(
|
|
||||||
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, $room, pubkey))),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const error of errors) {
|
|
||||||
if (error) {
|
|
||||||
return pushToast({theme: "error", message: errors[0]})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pushToast({message: "Members have successfully been added!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
src/app/components/SpaceActionItems.svelte
Normal file
58
src/app/components/SpaceActionItems.svelte
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
import Icon from "@lib/components/Icon.svelte"
|
||||||
|
import Modal from "@lib/components/Modal.svelte"
|
||||||
|
import ModalBody from "@lib/components/ModalBody.svelte"
|
||||||
|
import ModalHeader from "@lib/components/ModalHeader.svelte"
|
||||||
|
import ModalTitle from "@lib/components/ModalTitle.svelte"
|
||||||
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
|
import ReportItem from "@app/components/ReportItem.svelte"
|
||||||
|
import RoomJoinItem from "@app/components/RoomJoinItem.svelte"
|
||||||
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
|
import {REPORT} from "@welshman/util"
|
||||||
|
import {deriveSpaceActionItems} from "@app/core/state"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const {url}: Props = $props()
|
||||||
|
|
||||||
|
const actionItems = deriveSpaceActionItems(url)
|
||||||
|
|
||||||
|
const back = () => history.back()
|
||||||
|
|
||||||
|
const onResolved = () => {
|
||||||
|
if ($actionItems.length === 0) {
|
||||||
|
back()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<ModalBody>
|
||||||
|
<ModalHeader>
|
||||||
|
<ModalTitle>Action Items</ModalTitle>
|
||||||
|
<ModalSubtitle>on <RelayName {url} class="text-primary" /></ModalSubtitle>
|
||||||
|
</ModalHeader>
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
{#each $actionItems as event (event.id)}
|
||||||
|
{#if event.kind === REPORT}
|
||||||
|
<ReportItem {url} {event} {onResolved} />
|
||||||
|
{:else}
|
||||||
|
<RoomJoinItem {url} {event} {onResolved} />
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<p class="py-12 text-center">No action items found.</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</ModalBody>
|
||||||
|
<ModalFooter>
|
||||||
|
<Button class="btn btn-link" onclick={back}>
|
||||||
|
<Icon icon={AltArrowLeft} />
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
</ModalFooter>
|
||||||
|
</Modal>
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {displayRelayUrl, ManagementMethod} from "@welshman/util"
|
import {displayRelayUrl} from "@welshman/util"
|
||||||
import {manageRelay} from "@welshman/app"
|
|
||||||
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
import AltArrowLeft from "@assets/icons/alt-arrow-left.svg?dataurl"
|
||||||
import Spinner from "@lib/components/Spinner.svelte"
|
import Spinner from "@lib/components/Spinner.svelte"
|
||||||
import Button from "@lib/components/Button.svelte"
|
import Button from "@lib/components/Button.svelte"
|
||||||
|
|
@ -13,6 +12,7 @@
|
||||||
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
import ModalSubtitle from "@lib/components/ModalSubtitle.svelte"
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
import ProfileMultiSelect from "@app/components/ProfileMultiSelect.svelte"
|
||||||
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -27,23 +27,14 @@
|
||||||
loading = true
|
loading = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const results = await Promise.all(
|
const error = await addSpaceMembers(url, pubkeys)
|
||||||
pubkeys.map(pubkey =>
|
|
||||||
manageRelay(url, {
|
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
for (const {error} of results) {
|
if (error) {
|
||||||
if (error) {
|
pushToast({theme: "error", message: error})
|
||||||
return pushToast({theme: "error", message: error})
|
} else {
|
||||||
}
|
pushToast({message: "Members have successfully been added!"})
|
||||||
|
back()
|
||||||
}
|
}
|
||||||
|
|
||||||
pushToast({message: "Members have successfully been added!"})
|
|
||||||
back()
|
|
||||||
} finally {
|
} finally {
|
||||||
loading = false
|
loading = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
import ModalFooter from "@lib/components/ModalFooter.svelte"
|
||||||
import Profile from "@app/components/Profile.svelte"
|
import Profile from "@app/components/Profile.svelte"
|
||||||
import {deriveSpaceBannedPubkeyItems, deriveSupportedMethods} from "@app/core/state"
|
import {deriveSpaceBannedPubkeyItems, deriveSupportedMethods} from "@app/core/state"
|
||||||
|
import {addSpaceMembers} from "@app/core/commands"
|
||||||
import {pushToast} from "@app/util/toast"
|
import {pushToast} from "@app/util/toast"
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|
@ -55,10 +56,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const restoreMember = async (pubkey: string) => {
|
const restoreMember = async (pubkey: string) => {
|
||||||
const {error} = await manageRelay(url, {
|
const error = await addSpaceMembers(url, [pubkey])
|
||||||
method: ManagementMethod.AllowPubkey,
|
|
||||||
params: [pubkey],
|
|
||||||
})
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
pushToast({theme: "error", message: error})
|
pushToast({theme: "error", message: error})
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {onMount} from "svelte"
|
import {onMount} from "svelte"
|
||||||
import {derived} from "svelte/store"
|
import {derived} from "svelte/store"
|
||||||
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED, REPORT} from "@welshman/util"
|
import {displayRelayUrl, EVENT_TIME, ZAP_GOAL, THREAD, CLASSIFIED} from "@welshman/util"
|
||||||
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
import {deriveRelay, createSearch, pubkey} from "@welshman/app"
|
||||||
import {fly} from "@lib/transition"
|
import {fly} from "@lib/transition"
|
||||||
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
import Magnifier from "@assets/icons/magnifier.svg?dataurl"
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
import SpaceJoin from "@app/components/SpaceJoin.svelte"
|
||||||
import RelayName from "@app/components/RelayName.svelte"
|
import RelayName from "@app/components/RelayName.svelte"
|
||||||
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
import SpaceMembers from "@app/components/SpaceMembers.svelte"
|
||||||
import SpaceReports from "@app/components/SpaceReports.svelte"
|
import SpaceActionItems from "@app/components/SpaceActionItems.svelte"
|
||||||
import RoomCreate from "@app/components/RoomCreate.svelte"
|
import RoomCreate from "@app/components/RoomCreate.svelte"
|
||||||
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
import SpaceMenuRoomItem from "@app/components/SpaceMenuRoomItem.svelte"
|
||||||
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
import VoiceWidget from "@app/components/VoiceWidget.svelte"
|
||||||
|
|
@ -52,6 +52,7 @@
|
||||||
deriveUserCanCreateRoom,
|
deriveUserCanCreateRoom,
|
||||||
deriveUserIsSpaceAdmin,
|
deriveUserIsSpaceAdmin,
|
||||||
deriveEventsForUrl,
|
deriveEventsForUrl,
|
||||||
|
deriveSpaceActionItems,
|
||||||
notificationSettings,
|
notificationSettings,
|
||||||
deriveShouldNotify,
|
deriveShouldNotify,
|
||||||
displayRoom,
|
displayRoom,
|
||||||
|
|
@ -73,7 +74,7 @@
|
||||||
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
const otherVoiceRooms = deriveOtherVoiceRooms(url)
|
||||||
const members = deriveSpaceMembers(url)
|
const members = deriveSpaceMembers(url)
|
||||||
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
const userIsAdmin = deriveUserIsSpaceAdmin(url)
|
||||||
const reports = deriveEventsForUrl(url, [{kinds: [REPORT]}])
|
const actionItems = deriveSpaceActionItems(url)
|
||||||
|
|
||||||
const spaceKinds = derived(
|
const spaceKinds = derived(
|
||||||
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
deriveEventsForUrl(url, [{kinds: CONTENT_KINDS}]),
|
||||||
|
|
@ -102,7 +103,7 @@
|
||||||
|
|
||||||
const showMembers = () => pushModal(SpaceMembers, {url}, {replaceState})
|
const showMembers = () => pushModal(SpaceMembers, {url}, {replaceState})
|
||||||
|
|
||||||
const showReports = () => pushModal(SpaceReports, {url}, {replaceState})
|
const showActionItems = () => pushModal(SpaceActionItems, {url}, {replaceState})
|
||||||
|
|
||||||
const canCreateRoom = deriveUserCanCreateRoom(url)
|
const canCreateRoom = deriveUserCanCreateRoom(url)
|
||||||
|
|
||||||
|
|
@ -140,11 +141,15 @@
|
||||||
<SecondaryNavSection class="min-h-0 flex-1 flex flex-col overflow-hidden pb-0">
|
<SecondaryNavSection class="min-h-0 flex-1 flex flex-col overflow-hidden pb-0">
|
||||||
<div class="flex-shrink-0">
|
<div class="flex-shrink-0">
|
||||||
<Button
|
<Button
|
||||||
class="flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
class="relative flex w-full flex-col rounded-xl p-3 transition-all hover:bg-base-100"
|
||||||
onclick={openMenu}>
|
onclick={openMenu}>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<strong class="ellipsize flex items-center gap-1">
|
<strong class="flex items-center gap-1 relative">
|
||||||
<RelayName {url} />
|
<RelayName {url} class="ellipsize" />
|
||||||
|
<div
|
||||||
|
class="absolute -right-3 top-0 h-2 w-2 rounded-full bg-primary transition-all opacity-0"
|
||||||
|
class:opacity-100={$userIsAdmin && $actionItems.length > 0}>
|
||||||
|
</div>
|
||||||
{#if $notificationSettings.push && !$shouldNotify}
|
{#if $notificationSettings.push && !$shouldNotify}
|
||||||
<Icon icon={BellOff} size={3} class="opacity-50" />
|
<Icon icon={BellOff} size={3} class="opacity-50" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -178,9 +183,12 @@
|
||||||
</li>
|
</li>
|
||||||
{#if $userIsAdmin}
|
{#if $userIsAdmin}
|
||||||
<li>
|
<li>
|
||||||
<Button onclick={showReports}>
|
<Button onclick={showActionItems}>
|
||||||
<Icon icon={Danger} />
|
<Icon icon={Danger} />
|
||||||
View Reports ({$reports.length})
|
Action Items ({$actionItems.length})
|
||||||
|
{#if $actionItems.length > 0}
|
||||||
|
<div class="h-2 w-2 rounded-full bg-primary"></div>
|
||||||
|
{/if}
|
||||||
</Button>
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import {
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import {Nip01Signer} from "@welshman/signer"
|
import {Nip01Signer} from "@welshman/signer"
|
||||||
import type {UploadTask} from "@welshman/editor"
|
import type {UploadTask} from "@welshman/editor"
|
||||||
import type {TrustedEvent, EventContent, Profile} from "@welshman/util"
|
import type {TrustedEvent, EventContent, Profile, PublishedRoomMeta} from "@welshman/util"
|
||||||
import {
|
import {
|
||||||
DELETE,
|
DELETE,
|
||||||
REPORT,
|
REPORT,
|
||||||
|
|
@ -52,6 +52,7 @@ import {
|
||||||
editProfile,
|
editProfile,
|
||||||
createProfile,
|
createProfile,
|
||||||
uniqTags,
|
uniqTags,
|
||||||
|
ManagementMethod,
|
||||||
} from "@welshman/util"
|
} from "@welshman/util"
|
||||||
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
import {Pool, AuthStatus, SocketStatus} from "@welshman/net"
|
||||||
import {Router} from "@welshman/router"
|
import {Router} from "@welshman/router"
|
||||||
|
|
@ -72,6 +73,8 @@ import {
|
||||||
getPubkeyRelays,
|
getPubkeyRelays,
|
||||||
userBlossomServerList,
|
userBlossomServerList,
|
||||||
getThunkError,
|
getThunkError,
|
||||||
|
addRoomMember,
|
||||||
|
manageRelay,
|
||||||
} from "@welshman/app"
|
} from "@welshman/app"
|
||||||
import {compressFile} from "@lib/html"
|
import {compressFile} from "@lib/html"
|
||||||
import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
|
import type {SettingsValues, SpaceNotificationSettings} from "@app/core/state"
|
||||||
|
|
@ -89,6 +92,7 @@ import {
|
||||||
stripPrefix,
|
stripPrefix,
|
||||||
relaysMostlyRestricted,
|
relaysMostlyRestricted,
|
||||||
deriveSocket,
|
deriveSocket,
|
||||||
|
deriveSpaceMembers,
|
||||||
} from "@app/core/state"
|
} from "@app/core/state"
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
|
|
@ -220,8 +224,7 @@ export const attemptRelayAccess = async (url: string, claim = "") => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const thunk = publishJoinRequest({url, claim})
|
const error = await waitForThunkError(publishJoinRequest({url, claim}))
|
||||||
const error = await waitForThunkError(thunk)
|
|
||||||
|
|
||||||
if (shouldIgnoreError(error)) return
|
if (shouldIgnoreError(error)) return
|
||||||
if (!claim && error.includes("invite code size")) return
|
if (!claim && error.includes("invite code size")) return
|
||||||
|
|
@ -699,3 +702,50 @@ export const updateProfile = ({
|
||||||
|
|
||||||
return publishThunk({event, relays})
|
return publishThunk({event, relays})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Admin actions
|
||||||
|
|
||||||
|
export const addSpaceMembers = async (
|
||||||
|
url: string,
|
||||||
|
pubkeys: string[],
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const spaceMembers = get(deriveSpaceMembers(url))
|
||||||
|
const results = await Promise.all(
|
||||||
|
pubkeys
|
||||||
|
.filter(pubkey => !spaceMembers.includes(pubkey))
|
||||||
|
.map(pubkey =>
|
||||||
|
manageRelay(url, {
|
||||||
|
method: ManagementMethod.AllowPubkey,
|
||||||
|
params: [pubkey],
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const {error} of results) {
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addRoomMembers = async (
|
||||||
|
url: string,
|
||||||
|
room: PublishedRoomMeta,
|
||||||
|
pubkeys: string[],
|
||||||
|
): Promise<string | undefined> => {
|
||||||
|
const error = await addSpaceMembers(url, pubkeys)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = await Promise.all(
|
||||||
|
pubkeys.map(pubkey => waitForThunkError(addRoomMember(url, room, pubkey))),
|
||||||
|
)
|
||||||
|
|
||||||
|
for (const error of errors) {
|
||||||
|
if (error) {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,10 +47,10 @@ export const makeFeed = ({
|
||||||
onForwardExhausted?: () => void
|
onForwardExhausted?: () => void
|
||||||
at?: number
|
at?: number
|
||||||
}) => {
|
}) => {
|
||||||
const interval = int(WEEK)
|
|
||||||
const controller = new AbortController()
|
const controller = new AbortController()
|
||||||
const events = writable<TrustedEvent[]>([])
|
const events = writable<TrustedEvent[]>([])
|
||||||
|
|
||||||
|
let interval = int(WEEK)
|
||||||
let buffer: TrustedEvent[] = []
|
let buffer: TrustedEvent[] = []
|
||||||
let backwardWindow = [at - interval, at]
|
let backwardWindow = [at - interval, at]
|
||||||
let forwardWindow = [at, at + interval]
|
let forwardWindow = [at, at + interval]
|
||||||
|
|
@ -111,13 +111,20 @@ export const makeFeed = ({
|
||||||
}),
|
}),
|
||||||
]
|
]
|
||||||
|
|
||||||
const loadTimeframe = (since: number, until: number) => {
|
const loadTimeframe = async (since: number, until: number) => {
|
||||||
request({
|
const events = await request({
|
||||||
relays: [url],
|
relays: [url],
|
||||||
autoClose: true,
|
autoClose: true,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: filters.map(filter => ({...filter, since, until})),
|
filters: filters.map(filter => ({...filter, since, until})),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// If we found nothing, accelerate
|
||||||
|
if (events.length === 0) {
|
||||||
|
interval = Math.round(interval * 1.1)
|
||||||
|
} else {
|
||||||
|
interval = int(WEEK)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const backwardScroller = createScroller({
|
const backwardScroller = createScroller({
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ import {
|
||||||
groupBy,
|
groupBy,
|
||||||
remove,
|
remove,
|
||||||
simpleCache,
|
simpleCache,
|
||||||
|
removeUndefined,
|
||||||
} from "@welshman/lib"
|
} from "@welshman/lib"
|
||||||
import type {Override} from "@welshman/lib"
|
import type {Override} from "@welshman/lib"
|
||||||
import type {RepositoryUpdate} from "@welshman/net"
|
import type {RepositoryUpdate} from "@welshman/net"
|
||||||
|
|
@ -99,6 +100,7 @@ import {
|
||||||
REPOST,
|
REPOST,
|
||||||
GENERIC_REPOST,
|
GENERIC_REPOST,
|
||||||
asDecryptedEvent,
|
asDecryptedEvent,
|
||||||
|
getTagValue,
|
||||||
getGroupTags,
|
getGroupTags,
|
||||||
getListTags,
|
getListTags,
|
||||||
getPubkeyTagValues,
|
getPubkeyTagValues,
|
||||||
|
|
@ -111,6 +113,7 @@ import {
|
||||||
readRoomMeta,
|
readRoomMeta,
|
||||||
makeRoomMeta,
|
makeRoomMeta,
|
||||||
ManagementMethod,
|
ManagementMethod,
|
||||||
|
sortEventsAsc,
|
||||||
sortEventsDesc,
|
sortEventsDesc,
|
||||||
getAddress,
|
getAddress,
|
||||||
Address,
|
Address,
|
||||||
|
|
@ -287,7 +290,7 @@ export const deriveRelaySignedEvents = (url: string, filters: Filter[] = [{}]) =
|
||||||
derived(
|
derived(
|
||||||
[deriveRelay(url), deriveEventsForUrl(url, filters)],
|
[deriveRelay(url), deriveEventsForUrl(url, filters)],
|
||||||
([relay, events]) => events,
|
([relay, events]) => events,
|
||||||
// khatru doesn't support relay.self, uncomment when it's ready
|
// TODO: khatru doesn't support relay.self, uncomment when it's ready
|
||||||
// filter(spec({pubkey: relay.self}), events)
|
// filter(spec({pubkey: relay.self}), events)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -869,7 +872,7 @@ export const deriveRoomMembers = (url: string, h: string) => {
|
||||||
|
|
||||||
const members = new Set<string>()
|
const members = new Set<string>()
|
||||||
|
|
||||||
for (const event of sortBy(e => -e.created_at, $events)) {
|
for (const event of sortEventsAsc($events)) {
|
||||||
const pubkeys = getPubkeyTagValues(event.tags)
|
const pubkeys = getPubkeyTagValues(event.tags)
|
||||||
|
|
||||||
if (event.kind === ROOM_ADD_MEMBER) {
|
if (event.kind === ROOM_ADD_MEMBER) {
|
||||||
|
|
@ -903,6 +906,50 @@ export const deriveRoomAdmins = (url: string, h: string) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Action items (admin review queue)
|
||||||
|
// const pendingJoins: TrustedEvent[] = []
|
||||||
|
|
||||||
|
export const deriveSpaceActionItems = (url: string) =>
|
||||||
|
derived(
|
||||||
|
deriveEventsForUrl(url, [
|
||||||
|
{
|
||||||
|
kinds: [REPORT, ROOM_JOIN, ROOM_LEAVE, ROOM_MEMBERS],
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
$events => {
|
||||||
|
const getRoomId = (e: TrustedEvent) => getTagValue(e.kind === ROOM_MEMBERS ? "d" : "h", e.tags)
|
||||||
|
const reports = $events.filter(e => e.kind === REPORT)
|
||||||
|
const pendingJoins: TrustedEvent[] = []
|
||||||
|
|
||||||
|
// Room-level join requests — most recent per pubkey+h
|
||||||
|
for (const [h, roomEvents] of groupBy(getRoomId, $events)) {
|
||||||
|
if (!h) continue
|
||||||
|
|
||||||
|
const roomJoins = roomEvents.filter(spec({kind: ROOM_JOIN}))
|
||||||
|
const roomLeaves = roomEvents.filter(spec({kind: ROOM_LEAVE}))
|
||||||
|
const roomMembersEvent = roomEvents.find(spec({kind: ROOM_MEMBERS}))
|
||||||
|
const roomMembers = getTagValues("p", roomMembersEvent?.tags ?? [])
|
||||||
|
|
||||||
|
pendingJoins.push(
|
||||||
|
...removeUndefined(
|
||||||
|
Array.from(groupBy(e => e.pubkey, roomJoins).values())
|
||||||
|
.map(sortEventsDesc)
|
||||||
|
.map(first),
|
||||||
|
).filter(({pubkey, created_at}) => {
|
||||||
|
if (roomMembers.includes(pubkey)) return false
|
||||||
|
if (gt(roomMembersEvent?.created_at, created_at)) return false
|
||||||
|
if (roomLeaves.some(e => e.pubkey === pubkey && e.created_at > created_at))
|
||||||
|
return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return sortEventsDesc([...reports, ...pendingJoins])
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// User membership status
|
// User membership status
|
||||||
|
|
||||||
export enum MembershipStatus {
|
export enum MembershipStatus {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
ROOM_ADD_MEMBER,
|
ROOM_ADD_MEMBER,
|
||||||
ROOM_REMOVE_MEMBER,
|
ROOM_REMOVE_MEMBER,
|
||||||
ROOM_CREATE_PERMISSION,
|
ROOM_CREATE_PERMISSION,
|
||||||
|
ROOM_JOIN,
|
||||||
RELAY_MEMBERS,
|
RELAY_MEMBERS,
|
||||||
RELAY_ADD_MEMBER,
|
RELAY_ADD_MEMBER,
|
||||||
RELAY_REMOVE_MEMBER,
|
RELAY_REMOVE_MEMBER,
|
||||||
|
|
@ -268,7 +269,7 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||||
pullAndListen({
|
pullAndListen({
|
||||||
url,
|
url,
|
||||||
signal: controller.signal,
|
signal: controller.signal,
|
||||||
filters: [{kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS]}],
|
filters: [{kinds: [ROOM_META, ROOM_ADMINS, ROOM_MEMBERS, LIVEKIT_PARTICIPANTS, ROOM_JOIN]}],
|
||||||
})
|
})
|
||||||
|
|
||||||
// Room-scoped kinds: add #h tags when we know which rooms the user is in.
|
// Room-scoped kinds: add #h tags when we know which rooms the user is in.
|
||||||
|
|
@ -317,12 +318,6 @@ const syncSpace = (url: string, rooms: string[]) => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pullAndListen({
|
|
||||||
url,
|
|
||||||
signal: controller.signal,
|
|
||||||
filters: [{kinds: [LIVEKIT_PARTICIPANTS]}],
|
|
||||||
})
|
|
||||||
|
|
||||||
return () => controller.abort()
|
return () => controller.abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -381,22 +381,20 @@
|
||||||
<div class="py-20">
|
<div class="py-20">
|
||||||
<div class="card2 col-8 m-auto max-w-md items-center text-center">
|
<div class="card2 col-8 m-auto max-w-md items-center text-center">
|
||||||
<p class="opacity-75">You aren't currently a member of this room.</p>
|
<p class="opacity-75">You aren't currently a member of this room.</p>
|
||||||
{#if !$room.isClosed}
|
{#if $membershipStatus === MembershipStatus.Pending}
|
||||||
{#if $membershipStatus === MembershipStatus.Pending}
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
<Icon icon={ClockCircle} />
|
||||||
<Icon icon={ClockCircle} />
|
Access Pending
|
||||||
Access Pending
|
</Button>
|
||||||
</Button>
|
{:else}
|
||||||
{:else}
|
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
{#if joining}
|
||||||
{#if joining}
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
{:else}
|
||||||
{:else}
|
<Icon icon={Login2} />
|
||||||
<Icon icon={Login2} />
|
{/if}
|
||||||
{/if}
|
Join Room
|
||||||
Join Room
|
</Button>
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -456,22 +454,20 @@
|
||||||
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
{:else if $room.isRestricted && $membershipStatus !== MembershipStatus.Granted}
|
||||||
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
<div class="bg-alt card m-4 flex flex-row items-center justify-between px-4 py-3">
|
||||||
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
<p class="opacity-75">Only members are allowed to post to this room.</p>
|
||||||
{#if !$room.isClosed}
|
{#if $membershipStatus === MembershipStatus.Pending}
|
||||||
{#if $membershipStatus === MembershipStatus.Pending}
|
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={leaving} onclick={leave}>
|
<Icon icon={ClockCircle} />
|
||||||
<Icon icon={ClockCircle} />
|
Access Pending
|
||||||
Access Pending
|
</Button>
|
||||||
</Button>
|
{:else}
|
||||||
{:else}
|
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
||||||
<Button class="btn btn-neutral btn-sm" disabled={joining} onclick={join}>
|
{#if joining}
|
||||||
{#if joining}
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
<span class="loading loading-spinner loading-sm"></span>
|
{:else}
|
||||||
{:else}
|
<Icon icon={Login2} />
|
||||||
<Icon icon={Login2} />
|
{/if}
|
||||||
{/if}
|
Ask to Join
|
||||||
Ask to Join
|
</Button>
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue