diff --git a/.github/workflows/deploy-queue.yaml b/.github/workflows/deploy-queue.yaml deleted file mode 100644 index 5fffa437..00000000 --- a/.github/workflows/deploy-queue.yaml +++ /dev/null @@ -1,92 +0,0 @@ -name: deploy queue - -on: - push: - branches: - - canary - paths: - - fluxer_queue/** - - .github/workflows/deploy-queue.yaml - workflow_dispatch: - inputs: - ref: - type: string - required: false - default: '' - description: Optional git ref (defaults to the triggering branch) - -concurrency: - group: deploy-fluxer-queue - cancel-in-progress: true - -permissions: - contents: read - -jobs: - deploy: - name: Deploy queue - runs-on: blacksmith-8vcpu-ubuntu-2404 - timeout-minutes: 25 - env: - IS_CANARY: true - STACK: fluxer-queue - CACHE_SCOPE: deploy-fluxer-queue - RELEASE_CHANNEL: canary - - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ inputs.ref || '' }} - fetch-depth: 0 - - - name: Record deploy commit - run: python3 scripts/ci/workflows/deploy_queue.py --step record_deploy_commit - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - - - name: Set build timestamp - run: python3 scripts/ci/workflows/deploy_queue.py --step set_build_timestamp - - - name: Build image - uses: docker/build-push-action@v6 - with: - context: . - file: fluxer_queue/Dockerfile - tags: | - ${{ env.STACK }}:${{ env.DEPLOY_SHA }} - load: true - platforms: linux/amd64 - cache-from: type=gha,scope=${{ env.CACHE_SCOPE }} - cache-to: type=gha,mode=max,scope=${{ env.CACHE_SCOPE }} - build-args: | - BUILD_SHA=${{ env.DEPLOY_SHA }} - BUILD_NUMBER=${{ github.run_number }} - BUILD_TIMESTAMP=${{ env.BUILD_TIMESTAMP }} - RELEASE_CHANNEL=${{ env.RELEASE_CHANNEL }} - env: - DOCKER_BUILD_SUMMARY: false - DOCKER_BUILD_RECORD_UPLOAD: false - - - name: Install docker-pussh - run: python3 scripts/ci/workflows/deploy_queue.py --step install_docker_pussh - - - name: Set up SSH agent - uses: webfactory/ssh-agent@v0.9.1 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY_SERVER }} - - - name: Add server to known hosts - run: python3 scripts/ci/workflows/deploy_queue.py --step add_known_hosts --server-ip ${{ secrets.SERVER_IP }} - - - name: Push image and deploy - env: - SERVER: ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }} - IMAGE_TAG: ${{ env.STACK }}:${{ env.DEPLOY_SHA }} - run: python3 scripts/ci/workflows/deploy_queue.py --step push_and_deploy diff --git a/.gitignore b/.gitignore index 7825f1b3..0de62abf 100644 --- a/.gitignore +++ b/.gitignore @@ -23,7 +23,7 @@ devenv.local.nix /dev/livekit.yaml /dev/bluesky_oauth_key.pem /dev/meilisearch_master_key -/dev/data/meilisearch/ +/dev/data/ **/.dev.vars **/.DS_Store **/.env diff --git a/config/config.dev.template.json b/config/config.dev.template.json index f2c567cb..9a984841 100644 --- a/config/config.dev.template.json +++ b/config/config.dev.template.json @@ -37,20 +37,15 @@ }, "gateway": { "port": 49107, - "rpc_tcp_port": 49108, - "api_host": "http://localhost:49319/api", "admin_reload_secret": "", "media_proxy_endpoint": "http://localhost:49319/media", "logger_level": "debug" }, - "queue": { - "secret": "" + "nats": { + "core_url": "nats://127.0.0.1:4222", + "jetstream_url": "nats://127.0.0.1:4223" } }, - "gateway": { - "rpc_endpoint": "http://localhost:49107", - "rpc_secret": "" - }, "auth": { "sudo_mode_secret": "", "connection_initiation_secret": "", diff --git a/config/config.production.template.json b/config/config.production.template.json index b7866495..5bf09f23 100644 --- a/config/config.production.template.json +++ b/config/config.production.template.json @@ -16,7 +16,7 @@ "s3": { "access_key_id": "YOUR_S3_ACCESS_KEY", "secret_access_key": "YOUR_S3_SECRET_KEY", - "endpoint": "http://127.0.0.1:3900" + "endpoint": "http://127.0.0.1:8080/s3" }, "services": { "server": { @@ -36,19 +36,15 @@ }, "gateway": { "port": 8082, - "rpc_tcp_port": 8083, - "api_host": "http://127.0.0.1:8080/api", "admin_reload_secret": "GENERATE_A_64_CHAR_HEX_SECRET", "media_proxy_endpoint": "http://127.0.0.1:8080/media" }, - "queue": { - "secret": "GENERATE_A_64_CHAR_HEX_SECRET" + "nats": { + "core_url": "nats://nats:4222", + "jetstream_url": "nats://nats:4222", + "auth_token": "GENERATE_A_NATS_AUTH_TOKEN" } }, - "gateway": { - "rpc_endpoint": "http://127.0.0.1:8082", - "rpc_secret": "GENERATE_A_64_CHAR_HEX_SECRET" - }, "auth": { "sudo_mode_secret": "GENERATE_A_64_CHAR_HEX_SECRET", "connection_initiation_secret": "GENERATE_A_64_CHAR_HEX_SECRET", diff --git a/config/config.test.json b/config/config.test.json index 8dbc577c..6678f98f 100644 --- a/config/config.test.json +++ b/config/config.test.json @@ -23,18 +23,10 @@ "oauth_client_secret": "test-oauth-client-secret" }, "gateway": { - "rpc_tcp_port": 8089, - "api_host": "http://localhost:8088/api", "admin_reload_secret": "test-gateway-admin-reload-secret-32-chars", "media_proxy_endpoint": "http://localhost:8088/media" - }, - "queue": { - "secret": "test-queue-secret-key-minimum-32-chars" } }, - "gateway": { - "rpc_secret": "test-gateway-rpc-secret-minimum-32-chars" - }, "auth": { "sudo_mode_secret": "test-sudo-mode-secret-minimum-32-chars", "connection_initiation_secret": "test-connection-initiation-secret-32ch", diff --git a/devenv.nix b/devenv.nix index 242c25e9..885bc98c 100644 --- a/devenv.nix +++ b/devenv.nix @@ -96,6 +96,20 @@ restart = "always"; }; }; + nats_core = { + command = lib.mkForce "exec ${config.git.root}/scripts/dev_process_entry.sh nats_core nats-server -p 4222 -a 127.0.0.1"; + log_location = "${config.git.root}/dev/logs/nats_core.log"; + availability = { + restart = "always"; + }; + }; + nats_jetstream = { + command = lib.mkForce "exec ${config.git.root}/scripts/dev_process_entry.sh nats_jetstream nats-server -p 4223 -js -sd ${config.git.root}/dev/data/nats_jetstream -a 127.0.0.1"; + log_location = "${config.git.root}/dev/logs/nats_jetstream.log"; + availability = { + restart = "always"; + }; + }; }; }; }; @@ -107,6 +121,7 @@ rebar3 valkey meilisearch + nats-server ffmpeg exiftool caddy @@ -143,6 +158,8 @@ "devenv:processes:mailpit" "devenv:processes:valkey" "devenv:processes:caddy" + "devenv:processes:nats_core" + "devenv:processes:nats_jetstream" ]; }; @@ -229,5 +246,9 @@ caddy.exec = '' exec caddy run --config ${config.git.root}/dev/Caddyfile.dev --adapter caddyfile ''; + nats_core.exec = "exec nats-server -p 4222 -a 127.0.0.1"; + nats_jetstream.exec = '' + exec nats-server -p 4223 -js -sd ${config.git.root}/dev/data/nats_jetstream -a 127.0.0.1 + ''; }; } diff --git a/fluxer_app/src/Endpoints.tsx b/fluxer_app/src/Endpoints.tsx index b357a1eb..f97b081d 100644 --- a/fluxer_app/src/Endpoints.tsx +++ b/fluxer_app/src/Endpoints.tsx @@ -96,6 +96,11 @@ export const Endpoints = { CHANNEL_TYPING: (channelId: string) => `/channels/${channelId}/typing`, CHANNEL_WEBHOOKS: (channelId: string) => `/channels/${channelId}/webhooks`, CHANNEL_RTC_REGIONS: (channelId: string) => `/channels/${channelId}/rtc-regions`, + CHANNEL_CHUNKED_UPLOADS: (channelId: string) => `/channels/${channelId}/chunked-uploads`, + CHANNEL_CHUNKED_UPLOAD_CHUNK: (channelId: string, uploadId: string, chunkIndex: number) => + `/channels/${channelId}/chunked-uploads/${uploadId}/chunks/${chunkIndex}`, + CHANNEL_CHUNKED_UPLOAD_COMPLETE: (channelId: string, uploadId: string) => + `/channels/${channelId}/chunked-uploads/${uploadId}/complete`, CHANNEL_CALL: (channelId: string) => `/channels/${channelId}/call`, CHANNEL_CALL_RING: (channelId: string) => `/channels/${channelId}/call/ring`, CHANNEL_CALL_STOP_RINGING: (channelId: string) => `/channels/${channelId}/call/stop-ringing`, diff --git a/fluxer_app/src/components/channel/Messages.tsx b/fluxer_app/src/components/channel/Messages.tsx index 7d41eb53..3c9054d6 100644 --- a/fluxer_app/src/components/channel/Messages.tsx +++ b/fluxer_app/src/components/channel/Messages.tsx @@ -191,7 +191,7 @@ export const Messages = observer(function Messages({channel, onBottomBarVisibili canAutoAck, }); - useEffect(() => { + useLayoutEffect(() => { const node = messagesWrapperRef.current; if (node) { node.style.setProperty('--message-group-spacing', `${state.messageGroupSpacing}px`); diff --git a/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css b/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css index 028d500c..f2885bcc 100644 --- a/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css +++ b/fluxer_app/src/components/channel/message_search_bar/MessageSearchBar.module.css @@ -36,7 +36,7 @@ --search-scope-badge-hover-color: var(--text-primary); --search-scope-badge-border-color: var(--background-modifier-accent); --search-input-text-color: var(--text-primary); - --search-input-placeholder-color: var(--text-primary-muted); + --search-input-placeholder-color: var(--text-tertiary); --search-clear-button-color: var(--text-tertiary); --search-clear-button-hover-color: var(--text-primary); --search-clear-button-hover-background: var(--background-modifier-hover); diff --git a/fluxer_app/src/components/emojis/EmojiListItem.tsx b/fluxer_app/src/components/emojis/EmojiListItem.tsx index 06ca6f8e..6f9aab36 100644 --- a/fluxer_app/src/components/emojis/EmojiListItem.tsx +++ b/fluxer_app/src/components/emojis/EmojiListItem.tsx @@ -150,9 +150,10 @@ export const EmojiListItem: React.FC<{ guildId: string; emoji: GuildEmojiWithUser; layout: 'list' | 'grid'; + canModify: boolean; onRename: (emojiId: string, newName: string) => void; onRemove: (emojiId: string) => void; -}> = observer(({guildId, emoji, layout, onRename, onRemove}) => { +}> = observer(({guildId, emoji, layout, canModify, onRename, onRemove}) => { const {t} = useLingui(); const avatarUrl = emoji.user ? AvatarUtils.getUserAvatarURL(emoji.user, false) : null; const gridNameButtonRef = useRef(null); @@ -222,38 +223,44 @@ export const EmojiListItem: React.FC<{
- ( - - )} - > - - + + + ) : ( + :{emoji.name}: + )}
- - - - - + {canModify && ( + + + + + + )} ); } @@ -266,17 +273,21 @@ export const EmojiListItem: React.FC<{
- + {canModify ? ( + + ) : ( + :{emoji.name}: + )}
@@ -293,13 +304,15 @@ export const EmojiListItem: React.FC<{
- - - - - + {canModify && ( + + + + + + )} ); }); diff --git a/fluxer_app/src/components/form/Input.module.css b/fluxer_app/src/components/form/Input.module.css index 9e6d896d..ba0575ca 100644 --- a/fluxer_app/src/components/form/Input.module.css +++ b/fluxer_app/src/components/form/Input.module.css @@ -37,7 +37,7 @@ } .input::placeholder { - color: var(--text-primary-muted); + color: var(--text-tertiary); } .input.minHeight { @@ -209,7 +209,7 @@ } .textarea::placeholder { - color: var(--text-primary-muted); + color: var(--text-tertiary); } .textareaActions { diff --git a/fluxer_app/src/components/layout/UserArea.tsx b/fluxer_app/src/components/layout/UserArea.tsx index d194441c..7f586dd9 100644 --- a/fluxer_app/src/components/layout/UserArea.tsx +++ b/fluxer_app/src/components/layout/UserArea.tsx @@ -121,14 +121,28 @@ const UserAreaInner = observer( return; } - const height = voiceConnectionRef.current?.getBoundingClientRect().height ?? 0; - if (height > 0) { - root.style.setProperty(VOICE_CONNECTION_HEIGHT_VARIABLE, `${Math.round(height)}px`); - } else { + const element = voiceConnectionRef.current; + if (!element) { root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE); + return; } + const updateHeight = () => { + const height = element.getBoundingClientRect().height; + if (height > 0) { + root.style.setProperty(VOICE_CONNECTION_HEIGHT_VARIABLE, `${Math.round(height)}px`); + } else { + root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE); + } + }; + + updateHeight(); + + const observer = new ResizeObserver(updateHeight); + observer.observe(element); + return () => { + observer.disconnect(); root.style.removeProperty(VOICE_CONNECTION_HEIGHT_VARIABLE); }; }, [hasVoiceConnection]); @@ -164,13 +178,13 @@ const UserAreaInner = observer( return (
{hasVoiceConnection && ( - <> +
-
+
- +
)} {!hasVoiceConnection &&
}
diff --git a/fluxer_app/src/components/modals/AddConnectionModal.tsx b/fluxer_app/src/components/modals/AddConnectionModal.tsx index 886fd7d3..49c7b2ee 100644 --- a/fluxer_app/src/components/modals/AddConnectionModal.tsx +++ b/fluxer_app/src/components/modals/AddConnectionModal.tsx @@ -93,7 +93,10 @@ export const AddConnectionModal = observer(({defaultType}: AddConnectionModalPro const onSubmitInitiate = useCallback( async (data: InitiateFormInputs) => { - const identifier = data.identifier.trim(); + let identifier = data.identifier.trim(); + if (type === ConnectionTypes.BLUESKY) { + identifier = identifier.replace(/^https?:\/\/bsky\.app\/profile\//i, '').replace(/^@/, ''); + } if (UserConnectionStore.hasConnectionByTypeAndName(type, identifier)) { initiateForm.setError('identifier', {type: 'validate', message: t`You already have this connection.`}); return; diff --git a/fluxer_app/src/components/modals/GuildSettingsModal.tsx b/fluxer_app/src/components/modals/GuildSettingsModal.tsx index 12a4209f..a069686d 100644 --- a/fluxer_app/src/components/modals/GuildSettingsModal.tsx +++ b/fluxer_app/src/components/modals/GuildSettingsModal.tsx @@ -56,8 +56,11 @@ export const GuildSettingsModal: React.FC = observer( if (!guild) return guildSettingsTabs; return guildSettingsTabs.filter((tab) => { - if (tab.permission && !PermissionStore.can(tab.permission, {guildId})) { - return false; + if (tab.permission) { + const perms = Array.isArray(tab.permission) ? tab.permission : [tab.permission]; + if (!perms.some((p) => PermissionStore.can(p, {guildId}))) { + return false; + } } if (tab.requireFeature && !guild.features.has(tab.requireFeature)) { return false; diff --git a/fluxer_app/src/components/modals/InviteModal.tsx b/fluxer_app/src/components/modals/InviteModal.tsx index 38be6b39..741782df 100644 --- a/fluxer_app/src/components/modals/InviteModal.tsx +++ b/fluxer_app/src/components/modals/InviteModal.tsx @@ -192,9 +192,6 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => { }; const getExpirationText = () => { - if (maxAge === '0') { - return never expires; - } const option = maxAgeOptions.find((opt) => opt.value === maxAge); if (option) { switch (option.value) { @@ -310,9 +307,16 @@ export const InviteModal = observer(({channelId}: {channelId: string}) => { onInputClick={(e) => e.currentTarget.select()} inputProps={{placeholder: t`Invite link`}} > - {isUsingVanityUrl ? ( + {isUsingVanityUrl || maxAge === '0' ? (

- This invite link never expires. + This invite link never expires.{' '} + {!isUsingVanityUrl && ( + + + + )}

) : (

diff --git a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css index 23b4d9bf..8c8bdd6f 100644 --- a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css +++ b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.module.css @@ -87,7 +87,7 @@ margin: 0; overflow: hidden; display: -webkit-box; - -webkit-line-clamp: 2; + -webkit-line-clamp: 4; -webkit-box-orient: vertical; flex: 1; } diff --git a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx index c927d313..65862950 100644 --- a/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx +++ b/fluxer_app/src/components/modals/discovery/DiscoveryGuildCard.tsx @@ -19,11 +19,14 @@ import type {DiscoveryGuild} from '@app/actions/DiscoveryActionCreators'; import * as DiscoveryActionCreators from '@app/actions/DiscoveryActionCreators'; +import * as ModalActionCreators from '@app/actions/ModalActionCreators'; +import * as NavigationActionCreators from '@app/actions/NavigationActionCreators'; import * as ToastActionCreators from '@app/actions/ToastActionCreators'; import {GuildBadge} from '@app/components/guild/GuildBadge'; import styles from '@app/components/modals/discovery/DiscoveryGuildCard.module.css'; import {GuildIcon} from '@app/components/popouts/GuildIcon'; import {Button} from '@app/components/uikit/button/Button'; +import DiscoveryStore from '@app/stores/DiscoveryStore'; import GuildStore from '@app/stores/GuildStore'; import {getApiErrorMessage} from '@app/utils/ApiErrorUtils'; import {getCurrentLocale} from '@app/utils/LocaleUtils'; @@ -51,6 +54,9 @@ export const DiscoveryGuildCard = observer(function DiscoveryGuildCard({guild}: setJoining(true); try { await DiscoveryActionCreators.joinGuild(guild.id); + DiscoveryStore.reset(); + ModalActionCreators.pop(); + NavigationActionCreators.selectGuild(guild.id); } catch (error) { setJoining(false); const message = getApiErrorMessage(error) ?? t`Failed to join this community. Please try again.`; diff --git a/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx b/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx index 270313e2..d5699516 100644 --- a/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx +++ b/fluxer_app/src/components/modals/guild_tabs/GuildDiscoveryTab.tsx @@ -121,22 +121,22 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => { application.status === DiscoveryApplicationStatus.REJECTED || application.status === DiscoveryApplicationStatus.REMOVED); + const formValues = useMemo( + () => + hasActiveApplication && application + ? {description: application.description, category_type: application.category_type} + : undefined, + [hasActiveApplication, application], + ); + const form = useForm({ defaultValues: { - description: hasActiveApplication ? application.description : '', - category_type: hasActiveApplication ? application.category_type : 0, + description: '', + category_type: 0, }, + values: formValues, }); - useEffect(() => { - if (hasActiveApplication && application) { - form.reset({ - description: application.description, - category_type: application.category_type, - }); - } - }, [application, hasActiveApplication, form]); - const setApplicationFromResponse = useCallback((response: DiscoveryApplicationResponse) => { setStatus((prev) => (prev ? {...prev, application: response} : prev)); }, []); @@ -291,8 +291,10 @@ const GuildDiscoveryTab: React.FC<{guildId: string}> = ({guildId}) => {

Description
-