Compare commits

...

14 Commits

Author SHA1 Message Date
Jon Staab b14c3ab345 Bump version 2025-05-14 13:52:37 -07:00
Jon Staab 823058e335 Add setting for font size 2025-05-13 14:31:34 -07:00
Jon Staab 60ec6924f3 Fix thunks status layout 2025-05-13 10:35:42 -07:00
Jon Staab 18fc895fcb Tweak navigation to improve white labeled instances 2025-05-13 10:14:20 -07:00
Jon Staab 42295159a0 Update remove-pnpm-overrides to use package version of welshman (hack) 2025-05-13 09:06:53 -07:00
Jon Staab db408ac30d Stop propagation on thunk status 2025-05-12 15:35:13 -07:00
Jon Staab 1ced5689c3 Bump version 2025-05-12 15:19:38 -07:00
Jon Staab 263a803875 Add custom emoji parsing and display 2025-05-12 15:10:24 -07:00
Jon Staab 58afb8fa0c Bump editor 2025-05-12 11:17:13 -07:00
Jon Staab 4aaa19ea1b Apply theme to body so popovers get themed too, make selected popover item more clear 2025-05-12 10:03:29 -07:00
Jon Staab 2f9010cd13 Ignore unnecessary error 2025-05-12 09:01:13 -07:00
Jon Staab 12fcdfcd4f Add light theme and secondary color 2025-05-12 08:48:54 -07:00
Jon Staab 317ab57ed2 Use env instead of env.local 2025-05-12 08:27:46 -07:00
Jon Staab 52ef67740a Move default env to env.template, fix notifier relay/pubkey 2025-05-12 08:27:07 -07:00
37 changed files with 322 additions and 162 deletions
+3
View File
@@ -7,8 +7,11 @@ VITE_PLATFORM_NAME=Flotilla
VITE_PLATFORM_LOGO=static/flotilla.png
VITE_PLATFORM_RELAY=
VITE_PLATFORM_ACCENT="#7161FF"
VITE_PLATFORM_SECONDARY="#EB5E28"
VITE_PLATFORM_DESCRIPTION="Flotilla is nostr — for communities."
VITE_INDEXER_RELAYS=wss://purplepag.es/,wss://relay.damus.io/,wss://relay.nostr.band/
VITE_SIGNER_RELAYS=wss://relay.nsec.app/,wss://bucket.coracle.social/
VITE_NOTIFIER_PUBKEY=27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df
VITE_NOTIFIER_RELAY=wss://anchor.coracle.social/
VITE_GLITCHTIP_API_KEY=
GLITCHTIP_AUTH_TOKEN=
+1 -1
View File
@@ -1,5 +1,5 @@
# Env
.env.local
.env
# Vite
vite.config.js.timestamp-*
+15
View File
@@ -1,5 +1,20 @@
# Changelog
# 1.0.4
* Fix thunk status click handler
* Remove duplicate dependencies
* Improve navigation on white-labeled instances
* Add setting for font size
# 1.0.3
* Add light theme
* Use correct alerts server
* Ignore relay errors for claims
* Fix inline code blocks
* Add custom emoji parsing and display
# 1.0.2
* Fix add relay button
+3 -3
View File
@@ -14,7 +14,7 @@ To run your own Flotilla, it's as simple as:
## Environment
You can also optionally create an `.env.local` file and populate it with the following environment variables (see `.env` for examples):
You can also optionally create an `.env` file and populate it with the following environment variables (see `.env` for examples):
- `VITE_DEFAULT_PUBKEYS` - A comma-separated list of hex pubkeys for bootstrapping web of trust.
- `VITE_PLATFORM_URL` - The url where the app will be hosted. This is only used for build-time population of meta tags.
@@ -38,7 +38,7 @@ First, create an `A` record with your DNS provider pointing to the IP of your se
Next install `nginx`, `git`, and `certbot`. If you're on a debian- or ubuntu-based distro, run `sudo apt-get update && sudo apt-get install nginx git certbot python3-certbot-nginx`.
Now, create a new user where your code will be stored, clone the repository, fill in your `.env.local` file, and build the app.
Now, create a new user where your code will be stored, clone the repository, fill in your `.env` file, and build the app.
```sh
# Replace with your password
@@ -67,7 +67,7 @@ nvm install
nvm use
pnpm i
# Optionally create and populate .env.local to suit your use case
# Optionally create and populate .env to suit your use case
# Build the app
NODE_OPTIONS=--max_old_space_size=16384 pnpm run build
+2 -2
View File
@@ -7,8 +7,8 @@ android {
applicationId "social.flotilla"
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
versionCode 16
versionName "1.0.2"
versionCode 18
versionName "1.0.4"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
aaptOptions {
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
+4 -4
View File
@@ -2,12 +2,12 @@
temp_env=$(declare -p -x)
if [ -f .env ]; then
source .env
if [ -f .env.template ]; then
source .env.template
fi
if [ -f .env.local ]; then
source .env.local
if [ -f .env ]; then
source .env
fi
# Avoid overwriting env vars provided directly
+4 -4
View File
@@ -351,14 +351,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.4;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -376,14 +376,14 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 10;
CURRENT_PROJECT_VERSION = 11;
DEVELOPMENT_TEAM = S26U9DYW3A;
INFOPLIST_FILE = App/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = "Flotilla Chat";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.social-networking";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
MARKETING_VERSION = 1.0.2;
MARKETING_VERSION = 1.0.4;
PRODUCT_BUNDLE_IDENTIFIER = social.flotilla;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "flotilla",
"version": "1.0.2",
"version": "1.0.4",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -51,10 +51,10 @@
"@types/qrcode": "^1.5.5",
"@vite-pwa/assets-generator": "^0.2.6",
"@vite-pwa/sveltekit": "^0.6.6",
"@welshman/app": "^0.2.4",
"@welshman/content": "^0.2.1",
"@welshman/app": "^0.2.5",
"@welshman/content": "^0.2.2",
"@welshman/dvm": "^0.2.0",
"@welshman/editor": "^0.2.1",
"@welshman/editor": "^0.2.4",
"@welshman/feeds": "^0.2.2",
"@welshman/lib": "^0.2.2",
"@welshman/net": "^0.2.3",
@@ -62,7 +62,7 @@
"@welshman/router": "^0.2.0",
"@welshman/signer": "^0.2.3",
"@welshman/store": "^0.2.0",
"@welshman/util": "^0.2.2",
"@welshman/util": "^0.2.3",
"daisyui": "^4.12.10",
"date-picker-svelte": "^2.13.0",
"dotenv": "^16.4.5",
+1 -1
View File
@@ -1,8 +1,8 @@
import dotenv from "dotenv"
import {defineConfig, minimalPreset as preset} from "@vite-pwa/assets-generator/config"
dotenv.config({path: ".env.local"})
dotenv.config({path: ".env"})
dotenv.config({path: ".env.template"})
export default defineConfig({
preset,
+6 -1
View File
@@ -12,7 +12,12 @@ if (!pkgName?.endsWith("package.json")) {
const pkg = JSON.parse(fs.readFileSync(pkgName, "utf8"))
if (pkg.pnpm && pkg.pnpm.overrides) {
delete pkg.pnpm.overrides
// Use $package notation to make sure we only get one copy of each welshman dependency
// TODO: move welshman to a single package to straighten all this out.
for (const k of Object.keys(pkg.pnpm.overrides)) {
pkg.pnpm.overrides[k] = '$' + k
}
fs.writeFileSync(pkgName, JSON.stringify(pkg, null, 2) + "\n")
console.log("Removed pnpm.overrides from package.json")
} else {
+20 -10
View File
@@ -46,6 +46,14 @@
:root {
font-family: Lato;
--sait: env(safe-area-inset-top);
--saib: env(safe-area-inset-bottom);
--sail: env(safe-area-inset-left);
--sair: env(safe-area-inset-right);
}
[data-theme] {
@apply bg-base-300;
--base-100: oklch(var(--b1));
--base-200: oklch(var(--b2));
--base-300: oklch(var(--b3));
@@ -54,16 +62,6 @@
--primary-content: oklch(var(--pc));
--secondary: oklch(var(--s));
--secondary-content: oklch(var(--sc));
--sait: env(safe-area-inset-top);
--saib: env(safe-area-inset-bottom);
--sail: env(safe-area-inset-left);
--sair: env(safe-area-inset-right);
}
:root,
body,
html {
@apply bg-base-300;
}
/* safe area insets */
@@ -293,6 +291,14 @@ html {
--tiptap-active-fg: var(--base-content);
}
.tiptap-suggestions__item {
@apply border-l-2 border-solid border-base-100;
}
.tiptap-suggestions__selected {
@apply border-primary;
}
.tiptap {
@apply max-h-[350px] overflow-y-auto p-2 px-4;
}
@@ -382,6 +388,10 @@ progress[value]::-webkit-progress-value {
@apply w-full md:left-[18.5rem] md:w-[calc(100%-18.5rem-var(--sair))];
}
.cw-full {
@apply w-full md:left-[4rem] md:w-[calc(100%-4rem-var(--sair))];
}
.cb {
@apply md:bottom-sai bottom-[calc(var(--saib)+3.5rem)];
}
+10 -5
View File
@@ -276,9 +276,12 @@ export const checkRelayAccess = async (url: string, claim = "") => {
// If it's a strict NIP 29 relay don't worry about requesting access
// TODO: remove this if relay29 ever gets less strict
if (message !== "missing group (`h`) tag") {
return message
}
if (message === "missing group (`h`) tag") return
// Ignore messages about the relay ignoring ours
if (error?.startsWith("mute: ")) return
return message
}
}
@@ -377,10 +380,12 @@ export const publishReport = ({
export type ReactionParams = {
event: TrustedEvent
content: string
tags?: string[][]
}
export const makeReaction = ({event, content}: ReactionParams) => {
const tags = tagEventForReaction(event)
export const makeReaction = ({content, event, tags: paramTags = []}: ReactionParams) => {
const tags = [...paramTags, ...tagEventForReaction(event)]
const groupTag = getTag("h", event.tags)
if (groupTag) {
+5 -10
View File
@@ -1,5 +1,5 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {pubkey} from "@welshman/app"
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
@@ -26,20 +26,15 @@
const editEvent = () => pushModal(CalendarEventEdit, {url, event})
const onReactionClick = (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
if (reaction) {
publishDelete({relays: [url], event: reaction})
} else {
publishReaction({event, content, relays: [url]})
}
}
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
</script>
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-grow flex-wrap justify-end gap-2">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event} />
{#if showActivity}
<EventActivity {url} {path} {event} />
+11 -11
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {hash, now, formatTimestampAsTime, formatTimestampAsDate} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import {thunks, pubkey, deriveProfile, deriveProfileDisplay} from "@welshman/app"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {thunks, deriveProfile, deriveProfileDisplay} from "@welshman/app"
import {isMobile} from "@lib/html"
import TapTarget from "@lib/components/TapTarget.svelte"
import Avatar from "@lib/components/Avatar.svelte"
@@ -41,15 +41,10 @@
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey, url})
const onReactionClick = (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
if (reaction) {
publishDelete({relays: [url], event: reaction})
} else {
publishReaction({event, content, relays: [url]})
}
}
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
</script>
<TapTarget
@@ -89,7 +84,12 @@
</div>
</div>
<div class="row-2 ml-10 mt-1">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right" />
<ReactionSummary
{url}
{event}
{deleteReaction}
{createReaction}
reactionClass="tooltip-right" />
</div>
{#if !isMobile}
<button
+6 -7
View File
@@ -1,7 +1,7 @@
<script lang="ts">
import {type Instance} from "tippy.js"
import {hash, formatTimestampAsTime} from "@welshman/lib"
import type {TrustedEvent} from "@welshman/util"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {thunks, pubkey, deriveProfile, deriveProfileDisplay, sendWrapped} from "@welshman/app"
import {isMobile} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
@@ -36,12 +36,11 @@
const reply = () => replyTo(event)
const onReactionClick = async (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
const template = reaction ? makeDelete({event: reaction}) : makeReaction({event, content})
const deleteReaction = (event: TrustedEvent) =>
sendWrapped({template: makeDelete({event}), pubkeys})
await sendWrapped({template, pubkeys})
}
const createReaction = (template: EventContent) =>
sendWrapped({template: makeReaction({event, ...template}), pubkeys})
const openProfile = () => pushModal(ProfileDetail, {pubkey: event.pubkey})
@@ -120,7 +119,7 @@
</div>
</TapTarget>
<div class="row-2 z-feature -mt-4 ml-4">
<ReactionSummary {event} {onReactionClick} noTooltip />
<ReactionSummary {event} {deleteReaction} {createReaction} noTooltip />
</div>
</div>
</div>
+4
View File
@@ -6,6 +6,7 @@
truncate,
renderAsHtml,
isText,
isEmoji,
isTopic,
isCode,
isCashu,
@@ -22,6 +23,7 @@
import Icon from "@lib/components/Icon.svelte"
import Button from "@lib/components/Button.svelte"
import ContentToken from "@app/components/ContentToken.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte"
import ContentCode from "@app/components/ContentCode.svelte"
import ContentLinkInline from "@app/components/ContentLinkInline.svelte"
import ContentLinkBlock from "@app/components/ContentLinkBlock.svelte"
@@ -133,6 +135,8 @@
<ContentNewline value={parsed.value} />
{:else if isTopic(parsed)}
<ContentTopic value={parsed.value} />
{:else if isEmoji(parsed)}
<ContentEmoji value={parsed.value} />
{:else if isCode(parsed)}
<ContentCode
value={parsed.value}
+17
View File
@@ -0,0 +1,17 @@
<script lang="ts">
import type {ParsedEmojiValue} from "@welshman/content"
import {imgproxy} from "@app/state"
export let value: ParsedEmojiValue
const alt = `:${value.name}:`
</script>
{#if value.url}
<img
{alt}
src={imgproxy(value.url, {w: 24, h: 24})}
class="-mt-0.5 inline h-[1em] min-w-[1em] align-middle" />
{:else}
{alt}
{/if}
+15 -20
View File
@@ -5,35 +5,30 @@
import CardButton from "@lib/components/CardButton.svelte"
import MenuSpacesItem from "@app/components/MenuSpacesItem.svelte"
import SpaceAdd from "@app/components/SpaceAdd.svelte"
import {userRoomsByUrl, PLATFORM_RELAY} from "@app/state"
import {userRoomsByUrl} from "@app/state"
import {pushModal} from "@app/modal"
const addSpace = () => pushModal(SpaceAdd)
</script>
<div class="column menu gap-2">
{#if PLATFORM_RELAY}
<MenuSpacesItem url={PLATFORM_RELAY} />
<Divider />
{:else if $userRoomsByUrl.size > 0}
{#if $userRoomsByUrl.size > 0}
{#each $userRoomsByUrl.keys() as url (url)}
<MenuSpacesItem {url} />
{/each}
<Divider />
{/if}
{#if !PLATFORM_RELAY}
<Button onclick={addSpace}>
<CardButton>
{#snippet icon()}
<div><Icon icon="login-2" size={7} /></div>
{/snippet}
{#snippet title()}
<div>Add a space</div>
{/snippet}
{#snippet info()}
<div>Join or create a new space</div>
{/snippet}
</CardButton>
</Button>
{/if}
<Button onclick={addSpace}>
<CardButton>
{#snippet icon()}
<div><Icon icon="login-2" size={7} /></div>
{/snippet}
{#snippet title()}
<div>Add a space</div>
{/snippet}
{#snippet info()}
<div>Join or create a new space</div>
{/snippet}
</CardButton>
</Button>
</div>
+5 -11
View File
@@ -1,7 +1,6 @@
<script lang="ts">
import type {NativeEmoji} from "emoji-picker-element/shared"
import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app"
import type {TrustedEvent, EventContent} from "@welshman/util"
import Icon from "@lib/components/Icon.svelte"
import EmojiButton from "@lib/components/EmojiButton.svelte"
import NoteContent from "@app/components/NoteContent.svelte"
@@ -11,15 +10,10 @@
const {url, event} = $props()
const onReactionClick = (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
if (reaction) {
publishDelete({relays: [url], event: reaction})
} else {
publishReaction({event, content, relays: [url]})
}
}
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
const onEmoji = (emoji: NativeEmoji) =>
publishReaction({event, content: emoji.unicode, relays: [url]})
@@ -28,7 +22,7 @@
<NoteCard {event} {url} class="card2 bg-alt">
<NoteContent {event} expandMode="inline" />
<div class="flex w-full justify-between gap-2">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-right">
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-right">
<EmojiButton {onEmoji} class="btn btn-neutral btn-xs h-[26px] rounded-box">
<Icon icon="smile-circle" size={4} />
</EmojiButton>
+7 -7
View File
@@ -10,7 +10,7 @@
import Link from "@lib/components/Link.svelte"
import Profile from "@app/components/Profile.svelte"
import ProfileInfo from "@app/components/ProfileInfo.svelte"
import {makeChatPath} from "@app/routes"
import {pubkeyLink} from "@app/state"
type Props = {
pubkey: string
@@ -37,9 +37,9 @@
<div class="card2 bg-alt col-2 shadow-xl">
<div class="flex justify-between">
<Profile {pubkey} {url} />
<Link class="btn btn-primary hidden sm:flex" href={makeChatPath([pubkey])}>
<Icon icon="letter" />
Start a Chat
<Link external href={pubkeyLink(pubkey)} class="btn btn-primary hidden sm:flex">
<Icon icon="user-circle" />
See Complete Profile
</Link>
</div>
<ProfileInfo {pubkey} {url} />
@@ -48,8 +48,8 @@
Last active {formatTimestampRelative($events[0].created_at)}
</div>
{/if}
<Link class="btn btn-primary sm:hidden" href={makeChatPath([pubkey])}>
<Icon icon="letter" />
Start a Chat
<Link external href={pubkeyLink(pubkey)} class="btn btn-primary sm:hidden">
<Icon icon="user-circle" />
See Complete Profile
</Link>
</div>
+12 -5
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {page} from "$app/stores"
import {goto} from "$app/navigation"
import {splitAt} from "@welshman/lib"
@@ -16,8 +17,9 @@
import {pushModal} from "@app/modal"
import {makeSpacePath} from "@app/routes"
import {notifications} from "@app/notifications"
interface Props {
children?: import("svelte").Snippet
type Props = {
children?: Snippet
}
const {children}: Props = $props()
@@ -118,9 +120,14 @@
notification={$notifications.has("/chat")}>
<Avatar icon="letter" class="!h-10 !w-10" />
</PrimaryNavItem>
<PrimaryNavItem title="Spaces" onclick={showSpacesMenu} notification={anySpaceNotifications}>
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem>
{#if !PLATFORM_RELAY}
<PrimaryNavItem
title="Spaces"
onclick={showSpacesMenu}
notification={anySpaceNotifications}>
<Avatar icon="settings-minimalistic" class="!h-10 !w-10" />
</PrimaryNavItem>
{/if}
</div>
<PrimaryNavItem title="Settings" onclick={showSettingsMenu}>
<Avatar icon="settings" src={$userProfile?.picture} class="!h-10 !w-10" />
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
import {parse, isEmoji, renderAsHtml} from "@welshman/content"
import Icon from "@lib/components/Icon.svelte"
import ContentEmoji from "@app/components/ContentEmoji.svelte"
export let event
</script>
{#if event.content === "+" || event.content === ""}
<Icon icon="heart" />
{:else if event.content === "-"}
<Icon icon="thumbs-down" />
{:else}
{#each parse(event) as parsed}
{#if isEmoji(parsed)}
<ContentEmoji value={parsed.value} />
{:else}
{@html renderAsHtml(parsed)}
{/if}
{/each}
{/if}
+40 -13
View File
@@ -2,20 +2,29 @@
import {onMount} from "svelte"
import type {Snippet} from "svelte"
import {groupBy, uniq, uniqBy, batch, displayList} from "@welshman/lib"
import {REACTION, getReplyFilters, getTag, REPORT, DELETE} from "@welshman/util"
import type {TrustedEvent} from "@welshman/util"
import {
REACTION,
getReplyFilters,
getEmojiTags,
getEmojiTag,
getTag,
REPORT,
DELETE,
} from "@welshman/util"
import type {TrustedEvent, EventContent} from "@welshman/util"
import {deriveEvents} from "@welshman/store"
import {load} from "@welshman/net"
import {pubkey, repository, displayProfileByPubkey} from "@welshman/app"
import {isMobile, preventDefault, stopPropagation} from "@lib/html"
import Icon from "@lib/components/Icon.svelte"
import Reaction from "@app/components/Reaction.svelte"
import EventReportDetails from "@app/components/EventReportDetails.svelte"
import {displayReaction} from "@app/state"
import {pushModal} from "@app/modal"
interface Props {
event: any
onReactionClick: any
event: TrustedEvent
deleteReaction: (event: TrustedEvent) => void
createReaction: (event: EventContent) => void
url?: string
reactionClass?: string
noTooltip?: boolean
@@ -24,7 +33,8 @@
const {
event,
onReactionClick,
deleteReaction,
createReaction,
url = "",
reactionClass = "",
noTooltip = false,
@@ -39,14 +49,31 @@
filters: [{kinds: [REACTION], "#e": [event.id]}],
})
const onReactionClick = (events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
if (reaction) {
deleteReaction(reaction)
} else {
const [event] = events
createReaction({
content: event.content,
tags: getEmojiTags(event.content.replace(/:/g, ""), event.tags),
})
}
}
const onReportClick = () => pushModal(EventReportDetails, {url, event})
const reportReasons = $derived(uniq($reports.map(e => getTag("e", e.tags)?.[2])))
const getReactionKey = (e: TrustedEvent) => getEmojiTag(e.content, e.tags)?.join("") || e.content
const groupedReactions = $derived(
groupBy(
e => e.content,
uniqBy(e => e.pubkey + e.content, $reactions),
getReactionKey,
uniqBy(e => `${e.pubkey}${getReactionKey(e)}`, $reactions),
),
)
@@ -78,7 +105,7 @@
{#if url && $reports.length > 0}
<button
type="button"
data-tip="{`This content has been reported as "${displayList(reportReasons)}".`}}"
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}
onclick={stopPropagation(preventDefault(onReportClick))}>
@@ -86,12 +113,12 @@
<span>{$reports.length}</span>
</button>
{/if}
{#each groupedReactions.entries() as [content, events]}
{#each groupedReactions.entries() as [key, events]}
{@const pubkeys = events.map(e => e.pubkey)}
{@const isOwn = $pubkey && pubkeys.includes($pubkey)}
{@const info = displayList(pubkeys.map(pubkey => displayProfileByPubkey(pubkey)))}
{@const tooltip = `${info} reacted ${displayReaction(content)}`}
{@const onClick = () => onReactionClick(content, events)}
{@const tooltip = `${info} reacted`}
{@const onClick = () => onReactionClick(events)}
<button
type="button"
data-tip={tooltip}
@@ -101,7 +128,7 @@
class:border-solid={isOwn}
class:border-primary={isOwn}
onclick={stopPropagation(preventDefault(onClick))}>
<span>{displayReaction(content)}</span>
<Reaction event={events[0]} />
{#if events.length > 1}
<span>{events.length}</span>
{/if}
+5 -11
View File
@@ -1,6 +1,5 @@
<script lang="ts">
import type {TrustedEvent} from "@welshman/util"
import {pubkey} from "@welshman/app"
import type {TrustedEvent, EventContent} from "@welshman/util"
import ReactionSummary from "@app/components/ReactionSummary.svelte"
import ThunkStatusOrDeleted from "@app/components/ThunkStatusOrDeleted.svelte"
import EventActivity from "@app/components/EventActivity.svelte"
@@ -18,20 +17,15 @@
const path = makeThreadPath(url, event.id)
const onReactionClick = (content: string, events: TrustedEvent[]) => {
const reaction = events.find(e => e.pubkey === $pubkey)
const deleteReaction = (event: TrustedEvent) => publishDelete({relays: [url], event})
if (reaction) {
publishDelete({relays: [url], event: reaction})
} else {
publishReaction({event, content, relays: [url]})
}
}
const createReaction = (template: EventContent) =>
publishReaction({...template, event, relays: [url]})
</script>
<div class="flex flex-wrap items-center justify-between gap-2">
<div class="flex flex-grow flex-wrap justify-end gap-2">
<ReactionSummary {url} {event} {onReactionClick} reactionClass="tooltip-left" />
<ReactionSummary {url} {event} {deleteReaction} {createReaction} reactionClass="tooltip-left" />
<ThunkStatusOrDeleted {event} />
{#if showActivity}
<EventActivity {url} {path} {event} />
+10 -7
View File
@@ -1,5 +1,6 @@
<script lang="ts">
import {nth} from "@welshman/lib"
import {stopPropagation} from "svelte/legacy"
import {nth, noop} from "@welshman/lib"
import {PublishStatus} from "@welshman/net"
import {
MergedThunk,
@@ -60,9 +61,11 @@
{@const url = failedUrls[0]}
{@const status = $thunk.status[url]}
{@const message = $thunk.details[url]}
<div class="flex justify-end px-1 text-xs {restProps.class}">
<button
class="flex w-full justify-end px-1 text-xs {restProps.class}"
onclick={stopPropagation(noop)}>
<Tippy
class="flex items-center {restProps.class}"
class="flex items-center"
component={ThunkStatusDetail}
props={{url, message, status, retry}}
params={{interactive: true}}>
@@ -73,10 +76,10 @@
</span>
{/snippet}
</Tippy>
</div>
</button>
{:else if showPending}
<div class="flex justify-end px-1 text-xs {restProps.class}">
<span class="flex items-center gap-1 {restProps.class}">
<div class="flex w-full justify-end px-1 text-xs {restProps.class}">
<span class="flex items-center gap-1">
<span class="loading loading-spinner mx-1 h-3 w-3 translate-y-px"></span>
<span class="opacity-50">Sending...</span>
<button
@@ -84,7 +87,7 @@
class="underline transition-all"
class:link={canCancel}
class:opacity-25={!canCancel}
onclick={abort}>
onclick={stopPropagation(abort)}>
Cancel
</button>
</span>
+4 -3
View File
@@ -79,10 +79,9 @@ export const ALERT = 32830
export const ALERT_STATUS = 32831
export const NOTIFIER_PUBKEY = "27b7c2ed89ef78322114225ea3ebf5f72c7767c2528d4d0c1854d039c00085df"
export const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
// export const NOTIFIER_RELAY = 'wss://notifier.flotilla.social/'
export const NOTIFIER_RELAY = "ws://localhost:4738/"
export const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY
export const INDEXER_RELAYS = fromCsv(import.meta.env.VITE_INDEXER_RELAYS)
@@ -297,6 +296,7 @@ export type Settings = {
report_usage: boolean
report_errors: boolean
send_delay: number
font_size: number
}
}
@@ -306,6 +306,7 @@ export const defaultSettings = {
report_usage: true,
report_errors: true,
send_delay: 3000,
font_size: 1,
}
export const settings = deriveEventsMapped<Settings>(repository, {
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.0672 11.8568L20.4253 11.469L21.0672 11.8568ZM12.1432 2.93276L11.7553 2.29085V2.29085L12.1432 2.93276ZM21.25 12C21.25 17.1086 17.1086 21.25 12 21.25V22.75C17.9371 22.75 22.75 17.9371 22.75 12H21.25ZM12 21.25C6.89137 21.25 2.75 17.1086 2.75 12H1.25C1.25 17.9371 6.06294 22.75 12 22.75V21.25ZM2.75 12C2.75 6.89137 6.89137 2.75 12 2.75V1.25C6.06294 1.25 1.25 6.06294 1.25 12H2.75ZM15.5 14.25C12.3244 14.25 9.75 11.6756 9.75 8.5H8.25C8.25 12.5041 11.4959 15.75 15.5 15.75V14.25ZM20.4253 11.469C19.4172 13.1373 17.5882 14.25 15.5 14.25V15.75C18.1349 15.75 20.4407 14.3439 21.7092 12.2447L20.4253 11.469ZM9.75 8.5C9.75 6.41182 10.8627 4.5828 12.531 3.57467L11.7553 2.29085C9.65609 3.5593 8.25 5.86509 8.25 8.5H9.75ZM12 2.75C11.9115 2.75 11.8077 2.71008 11.7324 2.63168C11.6686 2.56527 11.6538 2.50244 11.6503 2.47703C11.6461 2.44587 11.6482 2.35557 11.7553 2.29085L12.531 3.57467C13.0342 3.27065 13.196 2.71398 13.1368 2.27627C13.0754 1.82126 12.7166 1.25 12 1.25V2.75ZM21.7092 12.2447C21.6444 12.3518 21.5541 12.3539 21.523 12.3497C21.4976 12.3462 21.4347 12.3314 21.3683 12.2676C21.2899 12.1923 21.25 12.0885 21.25 12H22.75C22.75 11.2834 22.1787 10.9246 21.7237 10.8632C21.286 10.804 20.7293 10.9658 20.4253 11.469L21.7092 12.2447Z" fill="#1C274C"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

+2 -4
View File
@@ -14,9 +14,7 @@
{@render props.input?.()}
</div>
</div>
<div class="scroll-container overflow-auto">
<div class="content-sizing">
{@render props.content?.()}
</div>
<div class="scroll-container content-sizing overflow-auto pt-2">
{@render props.content?.()}
</div>
</div>
+14 -6
View File
@@ -3,20 +3,28 @@
interface Props {
label?: Snippet
secondary?: Snippet
input?: Snippet
info?: Snippet
[key: string]: any
}
const {label, input, info, ...props}: Props = $props()
const {label, secondary, input, info, ...props}: Props = $props()
</script>
<div class="flex flex-col gap-2 {props.class}">
{#if label}
<label class="flex items-center gap-2 font-bold">
{@render label()}
</label>
{/if}
<div class="flex items-center justify-between">
{#if label}
<label class="flex items-center gap-2 font-bold">
{@render label()}
</label>
{/if}
{#if secondary}
<label class="flex items-center gap-2">
{@render secondary()}
</label>
{/if}
</div>
{@render input?.()}
{#if info}
<p class="text-sm">
+2
View File
@@ -59,6 +59,7 @@
import MapPoint from "@assets/icons/Map Point.svg?dataurl"
import MenuDots from "@assets/icons/Menu Dots.svg?dataurl"
import MenuDotsCircle from "@assets/icons/Menu Dots Circle.svg?dataurl"
import Moon from "@assets/icons/Moon.svg?dataurl"
import NotesMinimalistic from "@assets/icons/Notes Minimalistic.svg?dataurl"
import Pallete2 from "@assets/icons/Pallete 2.svg?dataurl"
import Paperclip from "@assets/icons/Paperclip.svg?dataurl"
@@ -151,6 +152,7 @@
"map-point": MapPoint,
"menu-dots": MenuDots,
"menu-dots-circle": MenuDotsCircle,
moon: Moon,
"notes-minimalistic": NotesMinimalistic,
"pallete-2": Pallete2,
paperclip: Paperclip,
+21 -4
View File
@@ -51,9 +51,15 @@
import {setupTracking} from "@app/tracking"
import {setupAnalytics} from "@app/analytics"
import {nsecDecode} from "@lib/util"
import {theme} from "@app/theme"
import {INDEXER_RELAYS, userMembership, ensureUnwrapped, canDecrypt} from "@app/state"
import {
INDEXER_RELAYS,
userMembership,
userSettingValues,
ensureUnwrapped,
canDecrypt,
} from "@app/state"
import {loadUserData, listenForNotifications} from "@app/requests"
import {theme} from "@app/theme"
import * as commands from "@app/commands"
import * as requests from "@app/requests"
import * as notifications from "@app/notifications"
@@ -121,6 +127,17 @@
}
}
// Sync theme
theme.subscribe($theme => {
document.body.setAttribute("data-theme", $theme)
})
// Sync font size
userSettingValues.subscribe($userSettingValues => {
// @ts-ignore
document.documentElement.style["font-size"] = `${$userSettingValues.font_size}rem`
})
if (!db) {
setupTracking()
setupAnalytics()
@@ -228,9 +245,9 @@
</svelte:head>
{#await ready}
<div data-theme={$theme}></div>
<div></div>
{:then}
<div data-theme={$theme}>
<div>
<AppContainer>
{@render children()}
</AppContainer>
+1 -1
View File
@@ -52,7 +52,7 @@
{/snippet}
</CardButton>
</Link>
<Link href="/people">
<Link href="/chat">
<CardButton>
{#snippet icon()}
<div><Icon icon="chat-round" size={7} /></div>
+1 -1
View File
@@ -28,7 +28,7 @@
})
</script>
<Page>
<Page class="cw-full">
<ContentSearch>
{#snippet input()}
<label class="row-2 input input-bordered">
+13 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import type {Snippet} from "svelte"
import {fly} from "@lib/transition"
import Icon from "@lib/components/Icon.svelte"
import Page from "@lib/components/Page.svelte"
@@ -7,13 +8,17 @@
import SecondaryNavSection from "@lib/components/SecondaryNavSection.svelte"
import LogOut from "@app/components/LogOut.svelte"
import {pushModal} from "@app/modal"
interface Props {
children?: import("svelte").Snippet
import {theme} from "@app/theme"
type Props = {
children?: Snippet
}
const {children}: Props = $props()
const logout = () => pushModal(LogOut)
const toggleTheme = () => theme.set($theme === "dark" ? "light" : "dark")
</script>
<SecondaryNav>
@@ -34,11 +39,16 @@
</SecondaryNavItem>
</div>
<div in:fly|local={{delay: 150}}>
<SecondaryNavItem onclick={toggleTheme}>
<Icon icon="moon" /> Theme
</SecondaryNavItem>
</div>
<div in:fly|local={{delay: 200}}>
<SecondaryNavItem href="/settings/about">
<Icon icon="info-square" /> About
</SecondaryNavItem>
</div>
<div in:fly|local={{delay: 200}}>
<div in:fly|local={{delay: 250}}>
<SecondaryNavItem class="text-error hover:text-error" onclick={logout}>
<Icon icon="exit" /> Log Out
</SecondaryNavItem>
+18
View File
@@ -166,6 +166,24 @@
<p>Choose a media server type and url for files you upload to {PLATFORM_NAME}.</p>
{/snippet}
</Field>
<strong class="text-lg">Accessibility</strong>
<Field>
{#snippet label()}
<p>Font size</p>
{/snippet}
{#snippet secondary()}
<p>{Math.round(settings.font_size * 100)}%</p>
{/snippet}
{#snippet input()}
<input
class="range range-primary"
type="range"
min="0.8"
max="1.3"
step="0.05"
bind:value={settings.font_size} />
{/snippet}
</Field>
<div class="mt-4 flex flex-row items-center justify-between gap-4">
<Button class="btn btn-neutral" onclick={reset}>Discard Changes</Button>
<Button type="submit" class="btn btn-primary">Save Changes</Button>
+10 -1
View File
@@ -2,8 +2,8 @@ import {config} from "dotenv"
import daisyui from "daisyui"
import themes from "daisyui/src/theming/themes"
config({path: ".env.local"})
config({path: ".env"})
config({path: ".env.template"})
/** @type {import('tailwindcss').Config} */
export default {
@@ -30,6 +30,15 @@ export default {
...themes["dark"],
primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY,
"secondary-content": "#EAE7FF",
},
light: {
...themes["winter"],
primary: process.env.VITE_PLATFORM_ACCENT,
"primary-content": "#EAE7FF",
secondary: process.env.VITE_PLATFORM_SECONDARY,
"secondary-content": "#EAE7FF",
},
},
],
+1 -1
View File
@@ -4,8 +4,8 @@ import {SvelteKitPWA} from "@vite-pwa/sveltekit"
import {sveltekit} from "@sveltejs/kit/vite"
import svg from "@poppanator/sveltekit-svg"
config({path: ".env.local"})
config({path: ".env"})
config({path: ".env.template"})
export default defineConfig({
server: {