From f77bd72c5743ff0bbc1cf7f0abca0dd6d4f48b10 Mon Sep 17 00:00:00 2001
From: Patrick Britton
Date: Wed, 4 Feb 2026 10:57:00 -0600
Subject: [PATCH] feat: Implement comprehensive reaction display widget, add
numerous new screens, services, models, documentation, and configuration
files.
---
.gitignore | 1 +
Caddyfile | 2 +-
.../supabase/functions/_shared/r2_signer.ts | 8 +-
.../cleanup-expired-content/index.ts | 2 +-
.../functions/deactivate-account/index.ts | 2 +-
.../functions/delete-account/index.ts | 2 +-
.../supabase/functions/manage-post/index.ts | 2 +-
.../supabase/functions/sign-media/index.ts | 2 +-
_legacy/supabase/functions/signup/index.ts | 2 +-
.../supabase/functions/tone-check/index.ts | 2 +-
html_landing/index.html | 6 +-
html_landing/privacy.html | 4 +-
migrations_archive/fix_remaining_url.sql | 16 +-
migrations_archive/repair_all_media_urls.sql | 18 +--
nginx/legacy_redirect.conf | 34 ++++
nginx/sojorn.conf | 4 +-
nginx/sojorn_final.conf | 18 +--
nginx/sojorn_net.conf | 34 ++++
nginx_sojorn_v2.conf | 10 +-
run_web.ps1 | 14 +-
run_web_chrome.ps1 | 14 +-
run_windows.ps1 | 16 +-
sojorn-api-http.conf | 2 +-
sojorn-api-v2.conf | 10 +-
sojorn-api.conf | 10 +-
.../android/app/src/main/AndroidManifest.xml | 2 +-
.../app/src/main/res/values/strings.xml | 4 +-
sojorn_app/lib/config/api_config.dart | 15 +-
sojorn_app/lib/models/notification.dart | 45 ++++--
sojorn_app/lib/models/search_results.dart | 15 +-
sojorn_app/lib/routes/app_routes.dart | 28 ++--
.../lib/screens/discover/discover_screen.dart | 117 ++++----------
sojorn_app/lib/screens/home/home_shell.dart | 32 +++-
.../notifications/notifications_screen.dart | 36 ++++-
.../post/threaded_conversation_screen.dart | 84 ++++++----
.../lib/screens/profile/profile_screen.dart | 2 +-
.../screens/quips/feed/quips_feed_screen.dart | 2 +-
.../lib/screens/search/search_screen.dart | 150 +++++++-----------
sojorn_app/lib/services/api_service.dart | 10 +-
.../lib/services/image_upload_service.dart | 4 +-
.../lib/services/notification_service.dart | 2 +-
sojorn_app/lib/utils/link_handler.dart | 2 +-
sojorn_app/lib/utils/url_launcher_helper.dart | 2 +-
.../lib/widgets/media/signed_media_image.dart | 6 +-
sojorn_app/lib/widgets/post/post_actions.dart | 10 +-
sojorn_app/lib/widgets/post/post_header.dart | 2 +-
.../widgets/reactions/reactions_display.dart | 12 +-
sojorn_app/run_chrome.bat | 2 +-
sojorn_app/run_dev.bat | 2 +-
.../BACKEND_MIGRATION_COMPREHENSIVE.md | 8 +-
sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md | 38 ++---
sojorn_docs/MIGRATION_STEP_BY_STEP.txt | 95 +++++++++++
sojorn_docs/SOJORN_ARCHITECTURE.md | 4 +-
sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md | 24 +--
sojorn_docs/deployment/VPS_SETUP_GUIDE.md | 28 ++--
.../legacy/BACKEND_MIGRATION_RUNBOOK.md | 2 +-
.../legacy/MIGRATION_VALIDATION_REPORT.md | 4 +-
sojorn_docs/reference/NEXT_STEPS.md | 12 +-
.../image-upload-fix-2025-01-08.md | 10 +-
.../test_image_upload_2025-01-05.md | 16 +-
60 files changed, 626 insertions(+), 436 deletions(-)
create mode 100644 nginx/legacy_redirect.conf
create mode 100644 nginx/sojorn_net.conf
create mode 100644 sojorn_docs/MIGRATION_STEP_BY_STEP.txt
diff --git a/.gitignore b/.gitignore
index 7b110f1..15cc788 100644
--- a/.gitignore
+++ b/.gitignore
@@ -153,3 +153,4 @@ temp_server.env
.zshrc
.profile
+sojorn_docs/SOJORN_ARCHITECTURE.md
diff --git a/Caddyfile b/Caddyfile
index babbfb3..59ca471 100644
--- a/Caddyfile
+++ b/Caddyfile
@@ -1,3 +1,3 @@
-api.gosojorn.com {
+api.sojorn.net {
reverse_proxy localhost:8080
}
diff --git a/_legacy/supabase/functions/_shared/r2_signer.ts b/_legacy/supabase/functions/_shared/r2_signer.ts
index 03dbd9b..ed820ce 100644
--- a/_legacy/supabase/functions/_shared/r2_signer.ts
+++ b/_legacy/supabase/functions/_shared/r2_signer.ts
@@ -1,7 +1,7 @@
import { AwsClient } from 'https://esm.sh/aws4fetch@1.0.17'
-const CUSTOM_MEDIA_DOMAIN = (Deno.env.get("CUSTOM_MEDIA_DOMAIN") ?? "https://img.gosojorn.com").trim();
-const CUSTOM_VIDEO_DOMAIN = (Deno.env.get("CUSTOM_VIDEO_DOMAIN") ?? "https://quips.gosojorn.com").trim();
+const CUSTOM_MEDIA_DOMAIN = (Deno.env.get("CUSTOM_MEDIA_DOMAIN") ?? "https://img.sojorn.net").trim();
+const CUSTOM_VIDEO_DOMAIN = (Deno.env.get("CUSTOM_VIDEO_DOMAIN") ?? "https://quips.sojorn.net").trim();
const DEFAULT_BUCKET_NAME = "sojorn-media";
const RESOLVED_BUCKET = (Deno.env.get("R2_BUCKET_NAME") ?? DEFAULT_BUCKET_NAME).trim();
@@ -36,8 +36,8 @@ export function transformLegacyMediaUrl(input: string): string | null {
try {
const url = new URL(trimmed);
- // Handle legacy media.gosojorn.com URLs
- if (url.hostname === 'media.gosojorn.com') {
+ // Handle legacy media.sojorn.net URLs
+ if (url.hostname === 'media.sojorn.net') {
const key = decodeURIComponent(url.pathname);
return key;
}
diff --git a/_legacy/supabase/functions/cleanup-expired-content/index.ts b/_legacy/supabase/functions/cleanup-expired-content/index.ts
index f8437ce..357345a 100644
--- a/_legacy/supabase/functions/cleanup-expired-content/index.ts
+++ b/_legacy/supabase/functions/cleanup-expired-content/index.ts
@@ -2,7 +2,7 @@ import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { S3Client, DeleteObjectCommand } from 'https://esm.sh/@aws-sdk/client-s3@3.470.0';
import { createServiceClient } from '../_shared/supabase-client.ts';
-const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com';
+const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net';
const corsHeaders = {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
diff --git a/_legacy/supabase/functions/deactivate-account/index.ts b/_legacy/supabase/functions/deactivate-account/index.ts
index ffbede7..451c0a5 100644
--- a/_legacy/supabase/functions/deactivate-account/index.ts
+++ b/_legacy/supabase/functions/deactivate-account/index.ts
@@ -11,7 +11,7 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createSupabaseClient } from '../_shared/supabase-client.ts';
-const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com';
+const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net';
serve(async (req) => {
if (req.method === 'OPTIONS') {
diff --git a/_legacy/supabase/functions/delete-account/index.ts b/_legacy/supabase/functions/delete-account/index.ts
index 0d70e1d..9d90ad7 100644
--- a/_legacy/supabase/functions/delete-account/index.ts
+++ b/_legacy/supabase/functions/delete-account/index.ts
@@ -12,7 +12,7 @@
import { serve } from 'https://deno.land/std@0.177.0/http/server.ts';
import { createSupabaseClient } from '../_shared/supabase-client.ts';
-const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com';
+const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net';
serve(async (req) => {
if (req.method === 'OPTIONS') {
diff --git a/_legacy/supabase/functions/manage-post/index.ts b/_legacy/supabase/functions/manage-post/index.ts
index 5d7e9a8..0a9108d 100644
--- a/_legacy/supabase/functions/manage-post/index.ts
+++ b/_legacy/supabase/functions/manage-post/index.ts
@@ -2,7 +2,7 @@
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
import { createSupabaseClient, createServiceClient } from '../_shared/supabase-client.ts';
-const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com';
+const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net';
const corsHeaders = {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
diff --git a/_legacy/supabase/functions/sign-media/index.ts b/_legacy/supabase/functions/sign-media/index.ts
index 96ff3f3..a26303b 100644
--- a/_legacy/supabase/functions/sign-media/index.ts
+++ b/_legacy/supabase/functions/sign-media/index.ts
@@ -55,7 +55,7 @@ serve(async (req) => {
});
}
- // Transform legacy media.gosojorn.com URLs to their object key
+ // Transform legacy media.sojorn.net URLs to their object key
const transformedTarget = transformLegacyMediaUrl(target) ?? target;
const signedUrl = await trySignR2Url(transformedTarget, expiresIn);
diff --git a/_legacy/supabase/functions/signup/index.ts b/_legacy/supabase/functions/signup/index.ts
index ac68b6c..8734474 100644
--- a/_legacy/supabase/functions/signup/index.ts
+++ b/_legacy/supabase/functions/signup/index.ts
@@ -26,7 +26,7 @@ serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response(null, {
headers: {
- 'Access-Control-Allow-Origin': Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com',
+ 'Access-Control-Allow-Origin': Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net',
'Access-Control-Allow-Methods': 'POST',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
},
diff --git a/_legacy/supabase/functions/tone-check/index.ts b/_legacy/supabase/functions/tone-check/index.ts
index 7828694..b916f32 100644
--- a/_legacy/supabase/functions/tone-check/index.ts
+++ b/_legacy/supabase/functions/tone-check/index.ts
@@ -3,7 +3,7 @@ import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
const OPENAI_MODERATION_URL = 'https://api.openai.com/v1/moderations'
-const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://gosojorn.com';
+const ALLOWED_ORIGIN = Deno.env.get('ALLOWED_ORIGIN') || 'https://sojorn.net';
const corsHeaders = {
'Access-Control-Allow-Origin': ALLOWED_ORIGIN,
diff --git a/html_landing/index.html b/html_landing/index.html
index b8f0d4e..2b1b0ba 100644
--- a/html_landing/index.html
+++ b/html_landing/index.html
@@ -49,7 +49,7 @@
@@ -119,8 +119,8 @@
- Join the waitlist by emailing waitlist@gosojorn.com
+ Join the waitlist by emailing waitlist@sojorn.net
diff --git a/html_landing/privacy.html b/html_landing/privacy.html
index b6fc06c..b05f504 100644
--- a/html_landing/privacy.html
+++ b/html_landing/privacy.html
@@ -73,8 +73,8 @@
you leave, you leave. We do not retain hidden profiles.
5. Contact
- For privacy concerns: privacy@gosojorn.com.
+ For privacy concerns: privacy@sojorn.net.
diff --git a/migrations_archive/fix_remaining_url.sql b/migrations_archive/fix_remaining_url.sql
index 0b97d72..2d2876e 100644
--- a/migrations_archive/fix_remaining_url.sql
+++ b/migrations_archive/fix_remaining_url.sql
@@ -1,28 +1,28 @@
--- Fix sojorn-media URLs (for images) to img.gosojorn.com
+-- Fix sojorn-media URLs (for images) to img.sojorn.net
UPDATE profiles
-SET avatar_url = REGEXP_REPLACE(avatar_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g')
+SET avatar_url = REGEXP_REPLACE(avatar_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g')
WHERE avatar_url LIKE '%r2.cloudflarestorage.com/sojorn-media%';
UPDATE profiles
-SET cover_url = REGEXP_REPLACE(cover_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g')
+SET cover_url = REGEXP_REPLACE(cover_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g')
WHERE cover_url LIKE '%r2.cloudflarestorage.com/sojorn-media%';
UPDATE posts
-SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g')
+SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g')
WHERE image_url LIKE '%r2.cloudflarestorage.com/sojorn-media%';
UPDATE posts
-SET thumbnail_url = REGEXP_REPLACE(thumbnail_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.gosojorn.com/', 'g')
+SET thumbnail_url = REGEXP_REPLACE(thumbnail_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-media/', 'https://img.sojorn.net/', 'g')
WHERE thumbnail_url LIKE '%r2.cloudflarestorage.com/sojorn-media%';
--- Fix sojorn-videos URLs (for quips) to quips.gosojorn.com
+-- Fix sojorn-videos URLs (for quips) to quips.sojorn.net
UPDATE posts
-SET video_url = REGEXP_REPLACE(video_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.gosojorn.com/', 'g')
+SET video_url = REGEXP_REPLACE(video_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.sojorn.net/', 'g')
WHERE video_url LIKE '%r2.cloudflarestorage.com/sojorn-videos%';
-- Fix the one edge case where image_url contains a video URL
UPDATE posts
-SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.gosojorn.com/', 'g')
+SET image_url = REGEXP_REPLACE(image_url, 'https://[a-zA-Z0-9]+\.r2\.cloudflarestorage\.com/sojorn-videos/', 'https://quips.sojorn.net/', 'g')
WHERE image_url LIKE '%r2.cloudflarestorage.com/sojorn-videos%';
-- Verify after
diff --git a/migrations_archive/repair_all_media_urls.sql b/migrations_archive/repair_all_media_urls.sql
index 00236f8..53a043c 100644
--- a/migrations_archive/repair_all_media_urls.sql
+++ b/migrations_archive/repair_all_media_urls.sql
@@ -3,7 +3,7 @@
-- 1. Fix image URLs that are just filenames (no domain)
UPDATE posts
-SET image_url = 'https://img.gosojorn.com/' || image_url
+SET image_url = 'https://img.sojorn.net/' || image_url
WHERE image_url IS NOT NULL
AND image_url != ''
AND image_url NOT LIKE 'http%'
@@ -11,7 +11,7 @@ WHERE image_url IS NOT NULL
-- 2. Fix video URLs that are just filenames (no domain)
UPDATE posts
-SET video_url = 'https://quips.gosojorn.com/' || video_url
+SET video_url = 'https://quips.sojorn.net/' || video_url
WHERE video_url IS NOT NULL
AND video_url != ''
AND video_url NOT LIKE 'http%'
@@ -19,7 +19,7 @@ WHERE video_url IS NOT NULL
-- 3. Fix thumbnail URLs that are just filenames
UPDATE posts
-SET thumbnail_url = 'https://img.gosojorn.com/' || thumbnail_url
+SET thumbnail_url = 'https://img.sojorn.net/' || thumbnail_url
WHERE thumbnail_url IS NOT NULL
AND thumbnail_url != ''
AND thumbnail_url NOT LIKE 'http%'
@@ -27,7 +27,7 @@ WHERE thumbnail_url IS NOT NULL
-- 4. Fix profile avatars that are just filenames
UPDATE profiles
-SET avatar_url = 'https://img.gosojorn.com/' || avatar_url
+SET avatar_url = 'https://img.sojorn.net/' || avatar_url
WHERE avatar_url IS NOT NULL
AND avatar_url != ''
AND avatar_url NOT LIKE 'http%'
@@ -35,7 +35,7 @@ WHERE avatar_url IS NOT NULL
-- 5. Fix profile covers that are just filenames
UPDATE profiles
-SET cover_url = 'https://img.gosojorn.com/' || cover_url
+SET cover_url = 'https://img.sojorn.net/' || cover_url
WHERE cover_url IS NOT NULL
AND cover_url != ''
AND cover_url NOT LIKE 'http%'
@@ -51,12 +51,12 @@ SELECT 'Profiles with covers' as check_type, count(*) as count FROM profiles WHE
-- Show any remaining non-standard URLs
SELECT 'Non-standard image URLs' as type, image_url FROM posts
WHERE image_url IS NOT NULL
- AND image_url NOT LIKE 'https://img.gosojorn.com/%'
- AND image_url NOT LIKE 'https://quips.gosojorn.com/%'
+ AND image_url NOT LIKE 'https://img.sojorn.net/%'
+ AND image_url NOT LIKE 'https://quips.sojorn.net/%'
LIMIT 5;
SELECT 'Non-standard video URLs' as type, video_url FROM posts
WHERE video_url IS NOT NULL
- AND video_url NOT LIKE 'https://img.gosojorn.com/%'
- AND video_url NOT LIKE 'https://quips.gosojorn.com/%'
+ AND video_url NOT LIKE 'https://img.sojorn.net/%'
+ AND video_url NOT LIKE 'https://quips.sojorn.net/%'
LIMIT 5;
diff --git a/nginx/legacy_redirect.conf b/nginx/legacy_redirect.conf
new file mode 100644
index 0000000..ff77f01
--- /dev/null
+++ b/nginx/legacy_redirect.conf
@@ -0,0 +1,34 @@
+# Redirect Legacy HTTP -> New HTTPS (gosojorn.com & gojorn.com)
+server {
+ server_name gosojorn.com api.gosojorn.com gojorn.com www.gojorn.com;
+ listen 80;
+ return 301 https://sojorn.net$request_uri;
+}
+
+# Redirect Legacy HTTPS -> New HTTPS (gosojorn.com ONLY)
+# We can only serve SSL for domains we have certs for (gosojorn)
+server {
+ server_name gosojorn.com;
+ listen 443 ssl;
+
+ # Use EXISTING legacy certificates
+ ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem;
+ include /etc/letsencrypt/options-ssl-nginx.conf;
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+
+ return 301 https://sojorn.net$request_uri;
+}
+
+server {
+ server_name api.gosojorn.com;
+ listen 443 ssl;
+
+ # Use EXISTING legacy certificates
+ ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem;
+ include /etc/letsencrypt/options-ssl-nginx.conf;
+ ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
+
+ return 301 https://api.sojorn.net$request_uri;
+}
diff --git a/nginx/sojorn.conf b/nginx/sojorn.conf
index 9da377d..35e3a3e 100644
--- a/nginx/sojorn.conf
+++ b/nginx/sojorn.conf
@@ -1,6 +1,6 @@
server {
listen 80;
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
location / {
proxy_pass http://localhost:8080;
@@ -12,7 +12,7 @@ server {
server {
listen 80;
- server_name gosojorn.com;
+ server_name sojorn.net;
root /var/www/sojorn;
index index.html;
diff --git a/nginx/sojorn_final.conf b/nginx/sojorn_final.conf
index 0102be3..cb429af 100644
--- a/nginx/sojorn_final.conf
+++ b/nginx/sojorn_final.conf
@@ -1,5 +1,5 @@
server {
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
location / {
proxy_pass http://localhost:8080;
@@ -14,14 +14,14 @@ server {
}
listen 443 ssl; # managed by Certbot
- ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem; # managed by Certbot
- ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem; # managed by Certbot
+ ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem; # managed by Certbot
+ ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
- server_name gosojorn.com;
+ server_name sojorn.net;
root /var/www/sojorn;
index index.html;
@@ -30,21 +30,21 @@ server {
}
listen 443 ssl; # managed by Certbot
- ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem; # managed by Certbot
- ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem; # managed by Certbot
+ ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem; # managed by Certbot
+ ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
- if ($host = api.gosojorn.com) {
+ if ($host = api.sojorn.net) {
return 301 https://$host$request_uri;
}
- if ($host = gosojorn.com) {
+ if ($host = sojorn.net) {
return 301 https://$host$request_uri;
}
listen 80;
- server_name api.gosojorn.com gosojorn.com;
+ server_name api.sojorn.net sojorn.net;
return 404;
}
diff --git a/nginx/sojorn_net.conf b/nginx/sojorn_net.conf
new file mode 100644
index 0000000..bd7adcd
--- /dev/null
+++ b/nginx/sojorn_net.conf
@@ -0,0 +1,34 @@
+# sojorn.net - Frontend
+server {
+ server_name sojorn.net www.sojorn.net;
+ root /var/www/sojorn;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
+ # Certbot will add SSL configuration here
+ listen 80;
+}
+
+# api.sojorn.net - Backend
+server {
+ server_name api.sojorn.net;
+
+ location / {
+ proxy_pass http://localhost:8080;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+
+ # WebSocket support
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection "upgrade";
+ }
+
+ # Certbot will add SSL configuration here
+ listen 80;
+}
diff --git a/nginx_sojorn_v2.conf b/nginx_sojorn_v2.conf
index b58b700..600f61f 100644
--- a/nginx_sojorn_v2.conf
+++ b/nginx_sojorn_v2.conf
@@ -1,5 +1,5 @@
server {
- server_name gosojorn.com www.gosojorn.com;
+ server_name sojorn.net www.sojorn.net;
root /var/www/sojorn;
index index.html;
@@ -28,18 +28,18 @@ server {
}
listen 443 ssl; # managed by Certbot
- ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
- if ($host = gosojorn.com) {
+ if ($host = sojorn.net) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
- server_name gosojorn.com www.gosojorn.com;
+ server_name sojorn.net www.sojorn.net;
return 404; # managed by Certbot
}
diff --git a/run_web.ps1 b/run_web.ps1
index 8361ea1..7bda38d 100644
--- a/run_web.ps1
+++ b/run_web.ps1
@@ -10,7 +10,7 @@ function Parse-Env($path) {
if (-not (Test-Path $path)) {
Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow
# Set default API_BASE_URL since no .env exists
- $vals['API_BASE_URL'] = 'https://api.gosojorn.com/api/v1'
+ $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1'
return $vals
}
Get-Content $path | ForEach-Object {
@@ -41,16 +41,18 @@ foreach ($k in $keysOfInterest) {
# Ensure API_BASE_URL is set
if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) {
- $currentApi = 'https://api.gosojorn.com/api/v1'
+ $currentApi = 'https://api.sojorn.net/api/v1'
$defineArgs += "--dart-define=API_BASE_URL=$currentApi"
-} else {
+}
+else {
$currentApi = $values['API_BASE_URL']
# Always ensure we're using the HTTPS endpoint
- if ($currentApi.StartsWith('http://api.gosojorn.com:8080')) {
- $currentApi = $currentApi.Replace('http://api.gosojorn.com:8080', 'https://api.gosojorn.com')
+ if ($currentApi.StartsWith('http://api.sojorn.net:8080')) {
+ $currentApi = $currentApi.Replace('http://api.sojorn.net:8080', 'https://api.sojorn.net')
$defineArgs = $defineArgs | Where-Object { -not ($_ -like '--dart-define=API_BASE_URL=*') }
$defineArgs += "--dart-define=API_BASE_URL=$currentApi"
- } elseif ($currentApi.StartsWith('http://localhost:')) {
+ }
+ elseif ($currentApi.StartsWith('http://localhost:')) {
# For local development, keep localhost but warn
Write-Host "Using local API: $currentApi" -ForegroundColor Yellow
}
diff --git a/run_web_chrome.ps1 b/run_web_chrome.ps1
index 70a996e..f71318e 100644
--- a/run_web_chrome.ps1
+++ b/run_web_chrome.ps1
@@ -10,7 +10,7 @@ function Parse-Env($path) {
if (-not (Test-Path $path)) {
Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow
# Set default API_BASE_URL since no .env exists
- $vals['API_BASE_URL'] = 'https://api.gosojorn.com/api/v1'
+ $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1'
return $vals
}
Get-Content $path | ForEach-Object {
@@ -41,16 +41,18 @@ foreach ($k in $keysOfInterest) {
# Ensure API_BASE_URL is set
if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) {
- $currentApi = 'https://api.gosojorn.com/api/v1'
+ $currentApi = 'https://api.sojorn.net/api/v1'
$defineArgs += "--dart-define=API_BASE_URL=$currentApi"
-} else {
+}
+else {
$currentApi = $values['API_BASE_URL']
# Always ensure we're using the HTTPS endpoint
- if ($currentApi.StartsWith('http://api.gosojorn.com:8080')) {
- $currentApi = $currentApi.Replace('http://api.gosojorn.com:8080', 'https://api.gosojorn.com')
+ if ($currentApi.StartsWith('http://api.sojorn.net:8080')) {
+ $currentApi = $currentApi.Replace('http://api.sojorn.net:8080', 'https://api.sojorn.net')
$defineArgs = $defineArgs | Where-Object { -not ($_ -like '--dart-define=API_BASE_URL=*') }
$defineArgs += "--dart-define=API_BASE_URL=$currentApi"
- } elseif ($currentApi.StartsWith('http://localhost:')) {
+ }
+ elseif ($currentApi.StartsWith('http://localhost:')) {
# For local development, keep localhost but warn
Write-Host "Using local API: $currentApi" -ForegroundColor Yellow
}
diff --git a/run_windows.ps1 b/run_windows.ps1
index 0f2ebce..492fd4b 100644
--- a/run_windows.ps1
+++ b/run_windows.ps1
@@ -9,7 +9,7 @@ function Parse-Env($path) {
if (-not (Test-Path $path)) {
Write-Host "No .env file found at ${path}. Using defaults." -ForegroundColor Yellow
# Set default API_BASE_URL since no .env exists
- $vals['API_BASE_URL'] = 'https://api.gosojorn.com/api/v1'
+ $vals['API_BASE_URL'] = 'https://api.sojorn.net/api/v1'
return $vals
}
Get-Content $path | ForEach-Object {
@@ -40,9 +40,10 @@ foreach ($k in $keysOfInterest) {
# Ensure API_BASE_URL is set
if (-not $values.ContainsKey('API_BASE_URL') -or [string]::IsNullOrWhiteSpace($values['API_BASE_URL'])) {
- $currentApi = 'https://api.gosojorn.com/api/v1'
+ $currentApi = 'https://api.sojorn.net/api/v1'
$defineArgs += "--dart-define=API_BASE_URL=$currentApi"
-} else {
+}
+else {
$currentApi = $values['API_BASE_URL']
if ($currentApi.StartsWith('http://localhost:')) {
# For local development, keep localhost but warn
@@ -71,7 +72,8 @@ try {
if ($Release) {
$cmdArgs += '--release'
- } else {
+ }
+ else {
$cmdArgs += '--debug'
}
@@ -99,10 +101,12 @@ try {
Write-Host "Starting Sojorn Windows app..." -ForegroundColor Yellow
Start-Process -FilePath $exePath
}
- } else {
+ }
+ else {
Write-Host "Build completed but executable not found at: $exePath" -ForegroundColor Red
}
- } else {
+ }
+ else {
Write-Host "Build failed!" -ForegroundColor Red
}
}
diff --git a/sojorn-api-http.conf b/sojorn-api-http.conf
index b1a66b8..13702ef 100644
--- a/sojorn-api-http.conf
+++ b/sojorn-api-http.conf
@@ -1,6 +1,6 @@
server {
listen 80;
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
# Allow Certbot to validate (it uses .well-known/acme-challenge)
location /.well-known/acme-challenge/ {
diff --git a/sojorn-api-v2.conf b/sojorn-api-v2.conf
index f94d79a..43b4eea 100644
--- a/sojorn-api-v2.conf
+++ b/sojorn-api-v2.conf
@@ -1,15 +1,15 @@
server {
listen 80;
- server_name api.gosojorn.com;
- return 301 https://api.gosojorn.com$request_uri;
+ server_name api.sojorn.net;
+ return 301 https://api.sojorn.net$request_uri;
}
server {
listen 443 ssl http2;
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
- ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
diff --git a/sojorn-api.conf b/sojorn-api.conf
index c92e6c7..23aedf1 100644
--- a/sojorn-api.conf
+++ b/sojorn-api.conf
@@ -1,15 +1,15 @@
server {
listen 80;
- server_name api.gosojorn.com;
- return 301 https://api.gosojorn.com$request_uri;
+ server_name api.sojorn.net;
+ return 301 https://api.sojorn.net$request_uri;
}
server {
listen 443 ssl http2;
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
- ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
diff --git a/sojorn_app/android/app/src/main/AndroidManifest.xml b/sojorn_app/android/app/src/main/AndroidManifest.xml
index c7d8a3b..ae38f06 100644
--- a/sojorn_app/android/app/src/main/AndroidManifest.xml
+++ b/sojorn_app/android/app/src/main/AndroidManifest.xml
@@ -43,7 +43,7 @@
-
+
diff --git a/sojorn_app/android/app/src/main/res/values/strings.xml b/sojorn_app/android/app/src/main/res/values/strings.xml
index 653bdd5..9f5dc2e 100644
--- a/sojorn_app/android/app/src/main/res/values/strings.xml
+++ b/sojorn_app/android/app/src/main/res/values/strings.xml
@@ -1,4 +1,4 @@
- chat_messages
- Chat messages
+ sojorn_notifications
+ Sojorn Notifications
diff --git a/sojorn_app/lib/config/api_config.dart b/sojorn_app/lib/config/api_config.dart
index 56c3347..33031c2 100644
--- a/sojorn_app/lib/config/api_config.dart
+++ b/sojorn_app/lib/config/api_config.dart
@@ -5,19 +5,24 @@ class ApiConfig {
static String _computeBaseUrl() {
final raw = const String.fromEnvironment(
'API_BASE_URL',
- defaultValue: 'https://api.gosojorn.com/api/v1',
+ defaultValue: 'https://api.sojorn.net/api/v1',
);
- // Auto-upgrade any lingering http://api.gosojorn.com:8080 (or plain http)
+ // Auto-upgrade any lingering http://api.sojorn.net:8080 (or plain http)
// to the public https endpoint behind nginx. This protects old .env files
// or cached web builds that still point at the closed port 8080.
- if (raw.startsWith('http://api.gosojorn.com:8080')) {
+ if (raw.startsWith('http://api.sojorn.net:8080')) {
return raw.replaceFirst(
- 'http://api.gosojorn.com:8080',
- 'https://api.gosojorn.com',
+ 'http://api.sojorn.net:8080',
+ 'https://api.sojorn.net',
);
}
+ // Belt-and-suspenders: Force migration even if args/env are stale
+ if (raw.contains('gosojorn.com')) {
+ return raw.replaceAll('gosojorn.com', 'sojorn.net');
+ }
+
if (raw.startsWith('http://')) {
return 'https://${raw.substring('http://'.length)}';
}
diff --git a/sojorn_app/lib/models/notification.dart b/sojorn_app/lib/models/notification.dart
index 04b360a..e1c63d2 100644
--- a/sojorn_app/lib/models/notification.dart
+++ b/sojorn_app/lib/models/notification.dart
@@ -2,14 +2,19 @@ import 'profile.dart';
/// Types of notifications
enum NotificationType {
- appreciate, // Someone appreciated your post
- chain, // Someone chained your post
- follow, // Someone followed you
- follow_request, // Someone requested to follow you
- new_follower, // Someone followed you (public or approved)
- request_accepted, // Someone accepted your follow request
- comment, // Someone commented on your post
- mention, // Someone mentioned you
+ like, // Someone liked your post
+ comment, // Someone commented on your post
+ reply, // Someone replied to your post (chained)
+ mention, // Someone mentioned you
+ follow, // Someone followed you
+ follow_request, // Someone requested to follow you
+ follow_accepted, // Someone accepted your follow request
+ message, // New chat message (if shown in notifications)
+ save, // Someone saved your post
+ beacon_vouch, // Someone vouched for your beacon
+ beacon_report, // Someone reported your beacon
+ share, // Someone shared your post
+ quip_reaction, // Someone reacted to your quip
}
/// Notification model
@@ -115,22 +120,32 @@ class AppNotification {
String get message {
final actorName = actor?.displayName ?? 'Someone';
switch (type) {
- case NotificationType.appreciate:
- return '$actorName appreciated your post';
- case NotificationType.chain:
- return '$actorName chained your post';
+ case NotificationType.like:
+ return '$actorName liked your post';
+ case NotificationType.reply:
+ return '$actorName replied to your post';
case NotificationType.follow:
return '$actorName started following you';
case NotificationType.follow_request:
return '$actorName requested to follow you';
- case NotificationType.new_follower:
- return '$actorName followed you';
- case NotificationType.request_accepted:
+ case NotificationType.follow_accepted:
return '$actorName accepted your follow request';
case NotificationType.comment:
return '$actorName commented on your post';
case NotificationType.mention:
return '$actorName mentioned you';
+ case NotificationType.message:
+ return '$actorName sent you a message';
+ case NotificationType.save:
+ return '$actorName saved your post';
+ case NotificationType.beacon_vouch:
+ return '$actorName vouched for your beacon';
+ case NotificationType.beacon_report:
+ return '$actorName reported your beacon';
+ case NotificationType.share:
+ return '$actorName shared your post';
+ case NotificationType.quip_reaction:
+ return '$actorName reacted to your quip';
}
}
}
diff --git a/sojorn_app/lib/models/search_results.dart b/sojorn_app/lib/models/search_results.dart
index d82e9ad..69fe84b 100644
--- a/sojorn_app/lib/models/search_results.dart
+++ b/sojorn_app/lib/models/search_results.dart
@@ -16,9 +16,9 @@ class SearchUser {
factory SearchUser.fromJson(Map json) {
return SearchUser(
- id: json['id'] as String,
- username: json['username'] as String,
- displayName: json['display_name'] as String? ?? json['displayName'] as String? ?? json['username'] as String,
+ id: json['id'] as String? ?? '',
+ username: (json['username'] as String?) ?? (json['handle'] as String?) ?? 'unknown',
+ displayName: json['display_name'] as String? ?? json['displayName'] as String? ?? json['handle'] as String? ?? json['username'] as String? ?? 'Unknown',
avatarUrl: json['avatar_url'] as String?,
harmonyTier: json['harmony_tier'] as String? ?? json['harmonyTier'] as String? ?? 'new',
);
@@ -81,12 +81,15 @@ class SearchPost {
});
factory SearchPost.fromJson(Map json) {
+ // Handle both flat structure and nested author object structure
+ final authorJson = json['author'] as Map?;
+
return SearchPost(
id: json['id'] as String,
body: json['body'] as String,
- authorId: json['author_id'] as String,
- authorHandle: json['author_handle'] as String,
- authorDisplayName: json['author_display_name'] as String,
+ authorId: json['author_id'] as String? ?? authorJson?['id'] as String? ?? '',
+ authorHandle: json['author_handle'] as String? ?? authorJson?['handle'] as String? ?? 'unknown',
+ authorDisplayName: json['author_display_name'] as String? ?? authorJson?['display_name'] as String? ?? 'Unknown',
createdAt: DateTime.parse(json['created_at'] as String),
);
}
diff --git a/sojorn_app/lib/routes/app_routes.dart b/sojorn_app/lib/routes/app_routes.dart
index a0ebe31..22b0ac6 100644
--- a/sojorn_app/lib/routes/app_routes.dart
+++ b/sojorn_app/lib/routes/app_routes.dart
@@ -19,6 +19,7 @@ import '../screens/profile/blocked_users_screen.dart';
import '../screens/auth/auth_gate.dart';
import '../screens/discover/discover_screen.dart';
import '../screens/secure_chat/secure_chat_full_screen.dart';
+import '../screens/post/threaded_conversation_screen.dart';
/// App routing config (GoRouter).
class AppRoutes {
@@ -65,6 +66,13 @@ class AppRoutes {
parentNavigatorKey: rootNavigatorKey,
builder: (_, __) => const SecureChatFullScreen(),
),
+ GoRoute(
+ path: '$postPrefix/:id',
+ parentNavigatorKey: rootNavigatorKey,
+ builder: (_, state) => ThreadedConversationScreen(
+ rootPostId: state.pathParameters['id'] ?? '',
+ ),
+ ),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) => AuthGate(
authenticatedChild: HomeShell(navigationShell: navigationShell),
@@ -81,16 +89,16 @@ class AppRoutes {
StatefulShellBranch(
routes: [
GoRoute(
- path: '/discover',
- builder: (_, __) => const DiscoverScreen(),
+ path: quips,
+ builder: (_, __) => const QuipsFeedScreen(),
),
],
),
StatefulShellBranch(
routes: [
GoRoute(
- path: quips,
- builder: (_, __) => const QuipsFeedScreen(),
+ path: '/beacon',
+ builder: (_, __) => const BeaconScreen(),
),
],
),
@@ -169,29 +177,29 @@ class AppRoutes {
}
/// Get shareable URL for a user profile
- /// Returns: https://gosojorn.com/u/username
+ /// Returns: https://sojorn.net/u/username
static String getProfileUrl(
String username, {
- String baseUrl = 'https://gosojorn.com',
+ String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/u/$username';
}
/// Get shareable URL for a post (future implementation)
- /// Returns: https://gosojorn.com/p/postid
+ /// Returns: https://sojorn.net/p/postid
static String getPostUrl(
String postId, {
- String baseUrl = 'https://gosojorn.com',
+ String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/p/$postId';
}
/// Get shareable URL for a beacon location
- /// Returns: https://gosojorn.com/beacon?lat=...&long=...
+ /// Returns: https://sojorn.net/beacon?lat=...&long=...
static String getBeaconUrl(
double lat,
double long, {
- String baseUrl = 'https://gosojorn.com',
+ String baseUrl = 'https://sojorn.net',
}) {
return '$baseUrl/beacon?lat=${lat.toStringAsFixed(6)}&long=${long.toStringAsFixed(6)}';
}
diff --git a/sojorn_app/lib/screens/discover/discover_screen.dart b/sojorn_app/lib/screens/discover/discover_screen.dart
index 9f7b220..3d5e72d 100644
--- a/sojorn_app/lib/screens/discover/discover_screen.dart
+++ b/sojorn_app/lib/screens/discover/discover_screen.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import '../../models/profile.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -81,9 +82,8 @@ class _DiscoverScreenState extends ConsumerState {
DiscoverData? discoverData;
List recentSearches = [];
int _searchEpoch = 0;
- final Map> _postFutures = {};
- static const Duration debounceDuration = Duration(milliseconds: 250);
+ static const Duration debounceDuration = Duration(milliseconds: 300);
@override
void initState() {
@@ -186,7 +186,9 @@ class _DiscoverScreenState extends ConsumerState {
}
debounceTimer = Timer(debounceDuration, () {
- performSearch(query);
+ if (query.length >= 2) {
+ performSearch(query);
+ }
});
}
@@ -259,18 +261,6 @@ class _DiscoverScreenState extends ConsumerState {
);
}
- Future _getPostFuture(String postId) {
- return _postFutures.putIfAbsent(postId, () {
- final apiService = ref.read(apiServiceProvider);
- return apiService.getPostById(postId);
- });
- }
-
- void _retryPostLoad(String postId) {
- _postFutures.remove(postId);
- if (mounted) setState(() {});
- }
-
void _openPostDetail(Post post) {
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
@@ -668,76 +658,35 @@ class _DiscoverScreenState extends ConsumerState {
}
Widget _buildPostResultItem(SearchPost post) {
- return FutureBuilder(
- future: _getPostFuture(post.id),
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.waiting) {
- return Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: AppTheme.cardSurface,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)),
- ),
- child: Row(
- children: [
- SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: AppTheme.royalPurple,
- ),
- ),
- const SizedBox(width: 12),
- Text('Loading post...', style: AppTheme.bodyMedium),
- ],
- ),
- );
- }
+ // Convert SearchPost to minimal Post immediately
+ final minimalPost = Post(
+ id: post.id,
+ body: post.body,
+ authorId: post.authorId,
+ createdAt: post.createdAt,
+ status: PostStatus.active,
+ detectedTone: ToneLabel.neutral,
+ contentIntegrityScore: 0.0,
+ author: Profile(
+ id: post.authorId,
+ handle: post.authorHandle,
+ displayName: post.authorDisplayName,
+ createdAt: DateTime.now(),
+ avatarUrl: null,
+ ),
+ isLiked: false,
+ likeCount: 0,
+ commentCount: 0,
+ tags: [],
+ );
- if (snapshot.hasError || !snapshot.hasData) {
- return Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: AppTheme.cardSurface,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)),
- ),
- child: Row(
- children: [
- Icon(Icons.error_outline,
- color: AppTheme.egyptianBlue.withOpacity(0.6)),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- 'Unable to load post',
- style: AppTheme.bodyMedium,
- ),
- ),
- TextButton(
- onPressed: () => _retryPostLoad(post.id),
- child: Text('Retry',
- style: AppTheme.labelMedium
- .copyWith(color: AppTheme.royalPurple)),
- ),
- ],
- ),
- );
- }
-
- final fullPost = snapshot.data!;
- return Padding(
- padding: const EdgeInsets.only(bottom: 12),
- child: sojornPostCard(
- post: fullPost,
- onTap: () => _openPostDetail(fullPost),
- onChain: () => _openChainComposer(fullPost),
- ),
- );
- },
+ return Padding(
+ padding: const EdgeInsets.only(bottom: 12),
+ child: sojornPostCard(
+ post: minimalPost,
+ onTap: () => _openPostDetail(minimalPost),
+ onChain: () => _openChainComposer(minimalPost),
+ ),
);
}
}
diff --git a/sojorn_app/lib/screens/home/home_shell.dart b/sojorn_app/lib/screens/home/home_shell.dart
index 101d9a2..f52a778 100644
--- a/sojorn_app/lib/screens/home/home_shell.dart
+++ b/sojorn_app/lib/screens/home/home_shell.dart
@@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
+import 'dart:async';
+import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:go_router/go_router.dart';
+import '../../services/notification_service.dart';
import '../../services/secure_chat_service.dart';
import '../../theme/app_theme.dart';
import '../notifications/notifications_screen.dart';
@@ -28,18 +31,29 @@ class HomeShell extends StatefulWidget {
class _HomeShellState extends State with WidgetsBindingObserver {
bool _isRadialMenuVisible = false;
final SecureChatService _chatService = SecureChatService();
+ StreamSubscription? _notifSub;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
_chatService.startBackgroundSync();
+ _initNotificationListener();
+ }
+
+ void _initNotificationListener() {
+ _notifSub = NotificationService.instance.foregroundMessages.listen((message) {
+ if (mounted) {
+ NotificationService.instance.showNotificationBanner(context, message);
+ }
+ });
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_chatService.stopBackgroundSync();
+ _notifSub?.cancel();
super.dispose();
}
@@ -171,17 +185,17 @@ class _HomeShellState extends State with WidgetsBindingObserver {
label: 'Home',
),
_buildNavBarItem(
- icon: Icons.explore_outlined,
- activeIcon: Icons.explore,
+ icon: Icons.play_circle_outline,
+ activeIcon: Icons.play_circle,
index: 1,
- label: 'Discover',
+ label: 'Quips',
),
const SizedBox(width: 48),
_buildNavBarItem(
- icon: Icons.play_circle_outline,
- activeIcon: Icons.play_circle,
+ icon: Icons.sensors_outlined,
+ activeIcon: Icons.sensors,
index: 2,
- label: 'Quips',
+ label: 'Beacon',
),
_buildNavBarItem(
icon: Icons.person_outline,
@@ -222,7 +236,11 @@ class _HomeShellState extends State with WidgetsBindingObserver {
icon: Icon(Icons.search, color: AppTheme.navyBlue),
tooltip: 'Discover',
onPressed: () {
- widget.navigationShell.goBranch(1);
+ Navigator.of(context).push(
+ MaterialPageRoute(
+ builder: (_) => const DiscoverScreen(),
+ ),
+ );
},
),
IconButton(
diff --git a/sojorn_app/lib/screens/notifications/notifications_screen.dart b/sojorn_app/lib/screens/notifications/notifications_screen.dart
index ac29af1..15a4b18 100644
--- a/sojorn_app/lib/screens/notifications/notifications_screen.dart
+++ b/sojorn_app/lib/screens/notifications/notifications_screen.dart
@@ -243,8 +243,7 @@ class _NotificationsScreenState extends ConsumerState {
// Navigate based on notification type
switch (notification.type) {
case NotificationType.follow:
- case NotificationType.new_follower:
- case NotificationType.request_accepted:
+ case NotificationType.follow_accepted:
// Navigate to the follower's profile
if (notification.actor != null) {
Navigator.of(context).push(
@@ -255,9 +254,9 @@ class _NotificationsScreenState extends ConsumerState {
}
break;
- case NotificationType.appreciate:
+ case NotificationType.like:
case NotificationType.comment:
- case NotificationType.chain:
+ case NotificationType.reply:
case NotificationType.mention:
// Fetch the post and navigate to post detail
if (notification.postId != null) {
@@ -284,8 +283,18 @@ class _NotificationsScreenState extends ConsumerState {
}
}
break;
+ case NotificationType.message:
+ // For messages, navigate to chat screen
+ if (notification.metadata?['conversation_id'] != null) {
+ context.push('/secure-chat/${notification.metadata!['conversation_id']}');
+ } else {
+ context.push('/secure-chat');
+ }
+ break;
case NotificationType.follow_request:
break;
+ default:
+ break;
}
}
@@ -492,20 +501,19 @@ class _NotificationItem extends StatelessWidget {
Color iconColor;
switch (notification.type) {
- case NotificationType.appreciate:
+ case NotificationType.like:
iconData = Icons.favorite;
iconColor = AppTheme.brightNavy;
break;
- case NotificationType.chain:
+ case NotificationType.reply:
iconData = Icons.subdirectory_arrow_right;
iconColor = AppTheme.royalPurple;
break;
case NotificationType.follow:
- case NotificationType.new_follower:
iconData = Icons.person_add;
iconColor = AppTheme.ksuPurple;
break;
- case NotificationType.request_accepted:
+ case NotificationType.follow_accepted:
iconData = Icons.check_circle;
iconColor = AppTheme.brightNavy;
break;
@@ -521,6 +529,18 @@ class _NotificationItem extends StatelessWidget {
iconData = Icons.person_add;
iconColor = AppTheme.ksuPurple;
break;
+ case NotificationType.message:
+ iconData = Icons.message;
+ iconColor = AppTheme.egyptianBlue;
+ break;
+ case NotificationType.save:
+ iconData = Icons.bookmark;
+ iconColor = AppTheme.ksuPurple;
+ break;
+ default:
+ iconData = Icons.notifications;
+ iconColor = AppTheme.egyptianBlue;
+ break;
}
return Container(
diff --git a/sojorn_app/lib/screens/post/threaded_conversation_screen.dart b/sojorn_app/lib/screens/post/threaded_conversation_screen.dart
index 90678a1..d800048 100644
--- a/sojorn_app/lib/screens/post/threaded_conversation_screen.dart
+++ b/sojorn_app/lib/screens/post/threaded_conversation_screen.dart
@@ -13,6 +13,9 @@ import '../../theme/app_theme.dart';
import '../../widgets/post/interactive_reply_block.dart';
import '../../widgets/media/signed_media_image.dart';
import '../compose/compose_screen.dart';
+import '../discover/discover_screen.dart';
+import '../secure_chat/secure_chat_full_screen.dart';
+import 'package:share_plus/share_plus.dart';
class ThreadedConversationScreen extends ConsumerStatefulWidget {
final String rootPostId;
@@ -176,7 +179,31 @@ class _ThreadedConversationScreenState extends ConsumerState context.go(AppRoutes.homeAlias),
+ icon: Icon(Icons.home_outlined, color: AppTheme.navyBlue),
+ ),
+ IconButton(
+ onPressed: () => Navigator.of(context, rootNavigator: true).push(
+ MaterialPageRoute(
+ builder: (_) => const DiscoverScreen(),
+ fullscreenDialog: true,
+ ),
+ ),
+ icon: Icon(Icons.search, color: AppTheme.navyBlue),
+ ),
+ IconButton(
+ onPressed: () => Navigator.of(context, rootNavigator: true).push(
+ MaterialPageRoute(
+ builder: (_) => SecureChatFullScreen(),
+ fullscreenDialog: true,
+ ),
+ ),
+ icon: Icon(Icons.chat_bubble_outline, color: AppTheme.navyBlue),
+ ),
+ const SizedBox(width: 8),
+ ],
);
}
@@ -583,9 +610,12 @@ class _ThreadedConversationScreenState extends ConsumerState _toggleLike(post),
+ onPressed: () => _sharePost(post),
icon: Icon(
- isLiked ? Icons.favorite : Icons.favorite_border,
- color: isLiked ? Colors.red : AppTheme.textSecondary,
+ Icons.share_outlined,
+ color: AppTheme.textSecondary,
),
style: IconButton.styleFrom(
- backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.08),
+ backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.05),
+ minimumSize: const Size(44, 44),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
@@ -614,7 +645,8 @@ class _ThreadedConversationScreenState extends ConsumerState Map.from(post.reactions!),
);
- print('DEBUG: Seeded reaction counts: ${_reactionCountsByPost[post.id]}');
}
if (post.myReactions != null) {
_myReactionsByPost.putIfAbsent(post.id, () => post.myReactions!.toSet());
- print('DEBUG: Seeded my reactions: ${_myReactionsByPost[post.id]}');
}
if (post.reactionUsers != null) {
_reactionUsersByPost.putIfAbsent(
@@ -1020,19 +1046,12 @@ class _ThreadedConversationScreenState extends ConsumerState _reactionCountsFor(Post post) {
- // Debug: Check what we're getting from the post model
- print('DEBUG: _reactionCountsFor for post ${post.id}');
- print('DEBUG: post.reactions = ${post.reactions}');
- print('DEBUG: _reactionCountsByPost[${post.id}] = ${_reactionCountsByPost[post.id]}');
-
// Prefer local state for immediate updates after toggle reactions
final localState = _reactionCountsByPost[post.id];
if (localState != null) {
- print('DEBUG: Using local state: ${localState}');
return localState;
}
// Fall back to post model if no local state
- print('DEBUG: Using post.reactions: ${post.reactions}');
return post.reactions ?? {};
}
@@ -1063,22 +1082,16 @@ class _ThreadedConversationScreenState extends ConsumerState?;
final updatedMine = response['my_reactions'] as List?;
- print('DEBUG: Toggle reaction response: $response');
- print('DEBUG: updatedCounts: $updatedCounts');
- print('DEBUG: updatedMine: $updatedMine');
-
if (updatedCounts != null) {
setState(() {
_reactionCountsByPost[postId] = updatedCounts
.map((key, value) => MapEntry(key, value as int));
- print('DEBUG: Updated local reaction counts: ${_reactionCountsByPost[postId]}');
});
}
if (updatedMine != null) {
setState(() {
_myReactionsByPost[postId] =
updatedMine.map((item) => item.toString()).toSet();
- print('DEBUG: Updated local my reactions: ${_myReactionsByPost[postId]}');
});
}
} catch (_) {
@@ -1161,4 +1174,19 @@ class _ThreadedConversationScreenState extends ConsumerState _sharePost(Post post) async {
+ final handle = post.author?.handle ?? 'sojorn';
+ final text = '${post.body}\n\n— @$handle on sojorn';
+
+ try {
+ await Share.share(text);
+ } catch (e) {
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Unable to share right now.')),
+ );
+ }
+ }
+ }
}
diff --git a/sojorn_app/lib/screens/profile/profile_screen.dart b/sojorn_app/lib/screens/profile/profile_screen.dart
index a737f11..d97f24b 100644
--- a/sojorn_app/lib/screens/profile/profile_screen.dart
+++ b/sojorn_app/lib/screens/profile/profile_screen.dart
@@ -29,7 +29,7 @@ class ProfileScreen extends ConsumerStatefulWidget {
String _resolveAvatar(String? url) {
if (url == null || url.isEmpty) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
- return 'https://img.gosojorn.com/${url.replaceFirst(RegExp('^/'), '')}';
+ return 'https://img.sojorn.net/${url.replaceFirst(RegExp('^/'), '')}';
}
class _ProfileScreenState extends ConsumerState
diff --git a/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart b/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart
index 1ce6c85..08f9cba 100644
--- a/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart
+++ b/sojorn_app/lib/screens/quips/feed/quips_feed_screen.dart
@@ -90,7 +90,7 @@ class _QuipsFeedScreenState extends ConsumerState
bool _isUserPaused = false;
int _lastRefreshToken = 0;
- static const int _branchIndex = 2;
+ static const int _branchIndex = 1;
static const int _pageSize = 8;
@override
diff --git a/sojorn_app/lib/screens/search/search_screen.dart b/sojorn_app/lib/screens/search/search_screen.dart
index 1f1f8e9..262b2a0 100644
--- a/sojorn_app/lib/screens/search/search_screen.dart
+++ b/sojorn_app/lib/screens/search/search_screen.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import '../../models/profile.dart';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -31,14 +32,12 @@ class _SearchScreenState extends ConsumerState {
SearchResults? results;
List recentSearches = [];
int _searchEpoch = 0;
-
- final Map> _postFutures = {};
// Discovery State
bool _isDiscoveryLoading = false;
List _discoveryPosts = [];
- static const Duration debounceDuration = Duration(milliseconds: 250);
+ static const Duration debounceDuration = Duration(milliseconds: 300);
static const List trendingTags = [
'safety',
'wellness',
@@ -155,7 +154,9 @@ class _SearchScreenState extends ConsumerState {
}
debounceTimer = Timer(debounceDuration, () {
- performSearch(query);
+ if (query.length >= 2) {
+ performSearch(query);
+ }
});
}
@@ -170,9 +171,15 @@ class _SearchScreenState extends ConsumerState {
});
try {
+ print('[SearchScreen] Requesting search for: "$normalizedQuery"');
final apiService = ref.read(apiServiceProvider);
final searchResults = await apiService.search(normalizedQuery);
- if (!mounted || requestId != _searchEpoch) return;
+ if (!mounted || requestId != _searchEpoch) {
+ print('[SearchScreen] Request $requestId discarded (stale)');
+ return;
+ }
+
+ print('[SearchScreen] Results received. Users: ${searchResults.users.length}, Tags: ${searchResults.tags.length}, Posts: ${searchResults.posts.length}');
if (searchResults.users.isNotEmpty) {
await saveRecentSearch(RecentSearch(
@@ -196,6 +203,7 @@ class _SearchScreenState extends ConsumerState {
isLoading = false;
});
} catch (e) {
+ print('[SearchScreen] Search error: $e');
if (!mounted || requestId != _searchEpoch) return;
setState(() {
isLoading = false;
@@ -214,18 +222,6 @@ class _SearchScreenState extends ConsumerState {
focusNode.requestFocus();
}
- Future _getPostFuture(String postId) {
- return _postFutures.putIfAbsent(postId, () {
- final apiService = ref.read(apiServiceProvider);
- return apiService.getPostById(postId);
- });
- }
-
- void _retryPostLoad(String postId) {
- _postFutures.remove(postId);
- if (mounted) setState(() {});
- }
-
void _openPostDetail(Post post) {
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
@@ -588,85 +584,49 @@ class _SearchScreenState extends ConsumerState {
}
Widget buildPostResultItem(SearchPost post) {
- return FutureBuilder(
- future: _getPostFuture(post.id),
- builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.waiting) {
- return Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: AppTheme.cardSurface,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)),
- ),
- child: Row(
- children: [
- SizedBox(
- width: 20,
- height: 20,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: AppTheme.royalPurple,
- ),
- ),
- const SizedBox(width: 12),
- Text('Loading post...', style: AppTheme.bodyMedium),
- ],
- ),
- );
- }
-
- if (snapshot.hasError || !snapshot.hasData) {
- return Container(
- margin: const EdgeInsets.only(bottom: 12),
- padding: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: AppTheme.cardSurface,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)),
- ),
- child: Row(
- children: [
- Icon(Icons.error_outline,
- color: AppTheme.egyptianBlue.withOpacity(0.6)),
- const SizedBox(width: 12),
- Expanded(
- child: Text(
- 'Unable to load post',
- style: AppTheme.bodyMedium,
- ),
- ),
- TextButton(
- onPressed: () => _retryPostLoad(post.id),
- child: Text('Retry',
- style:
- AppTheme.labelMedium.copyWith(color: AppTheme.royalPurple)),
- ),
- ],
- ),
- );
- }
-
- final fullPost = snapshot.data!;
- return ClipRRect(
- borderRadius: BorderRadius.circular(12),
- child: Container(
- margin: const EdgeInsets.only(bottom: 12),
- decoration: BoxDecoration(
- color: AppTheme.cardSurface,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)),
- ),
- child: sojornPostCard(
- post: fullPost,
- onTap: () => _openPostDetail(fullPost),
- onChain: () => _openChainComposer(fullPost),
- ),
- ),
- );
- },
+ // Convert SearchPost to minimal Post immediately
+ final minimalPost = Post(
+ id: post.id,
+ body: post.body,
+ authorId: post.authorId,
+ createdAt: post.createdAt,
+
+ // REQUIRED fields missing previously
+ status: PostStatus.active,
+ detectedTone: ToneLabel.neutral,
+ contentIntegrityScore: 0.0,
+
+ author: Profile(
+ id: post.authorId,
+ handle: post.authorHandle,
+ displayName: post.authorDisplayName,
+ createdAt: DateTime.now(),
+ avatarUrl: null,
+ ),
+ // Set defaults for rest
+ isLiked: false,
+ likeCount: 0,
+ commentCount: 0,
+ tags: [],
);
+
+ return ClipRRect(
+ borderRadius: BorderRadius.circular(12),
+ child: Container(
+ margin: const EdgeInsets.only(bottom: 12),
+ decoration: BoxDecoration(
+ color: AppTheme.cardSurface,
+ borderRadius: BorderRadius.circular(12),
+ border: Border.all(color: AppTheme.egyptianBlue.withOpacity(0.2)),
+ ),
+ child: sojornPostCard(
+ post: minimalPost,
+ onTap: () => _openPostDetail(minimalPost),
+ onChain: () => _openChainComposer(minimalPost),
+ // showActions removed (not supported)
+ ),
+ ),
+ );
}
}
diff --git a/sojorn_app/lib/services/api_service.dart b/sojorn_app/lib/services/api_service.dart
index b297484..f8b960a 100644
--- a/sojorn_app/lib/services/api_service.dart
+++ b/sojorn_app/lib/services/api_service.dart
@@ -943,17 +943,25 @@ class ApiService {
final sanitizedQuery = SecurityUtils.limitText(SecurityUtils.sanitizeText(query), maxLength: 100);
if (!SecurityUtils.isValidInput(sanitizedQuery)) {
+ if (kDebugMode) print('[API] Invalid search query input: $query');
return SearchResults(users: [], tags: [], posts: []);
}
try {
+ if (kDebugMode) print('[API] Searching for: $sanitizedQuery');
final data = await callGoApi(
'/search',
method: 'GET',
queryParams: {'q': sanitizedQuery},
);
+ // if (kDebugMode) print('[API] Search raw response: ${jsonEncode(data)}');
return SearchResults.fromJson(data);
- } catch (_) {
+ } catch (e, stack) {
+ if (kDebugMode) {
+ print('[API] Search failed for query: "$query"');
+ print('Error: $e');
+ print('Stack: $stack');
+ }
// Return empty results on error
return SearchResults(users: [], tags: [], posts: []);
}
diff --git a/sojorn_app/lib/services/image_upload_service.dart b/sojorn_app/lib/services/image_upload_service.dart
index acafad5..3f11e57 100644
--- a/sojorn_app/lib/services/image_upload_service.dart
+++ b/sojorn_app/lib/services/image_upload_service.dart
@@ -505,13 +505,13 @@ class ImageUploadService {
// Fix Image URLs
if (url.contains('/sojorn-media/')) {
final key = url.split('/sojorn-media/').last;
- return 'https://img.gosojorn.com/$key';
+ return 'https://img.sojorn.net/$key';
}
// Fix Video URLs
if (url.contains('/sojorn-videos/')) {
final key = url.split('/sojorn-videos/').last;
- return 'https://quips.gosojorn.com/$key';
+ return 'https://quips.sojorn.net/$key';
}
return url;
diff --git a/sojorn_app/lib/services/notification_service.dart b/sojorn_app/lib/services/notification_service.dart
index 61da650..05b380a 100644
--- a/sojorn_app/lib/services/notification_service.dart
+++ b/sojorn_app/lib/services/notification_service.dart
@@ -531,7 +531,7 @@ class NotificationService {
case 'thread_view':
case 'main_feed':
default:
- navigator.context.go(AppRoutes.home);
+ navigator.context.push('${AppRoutes.postPrefix}/$postId');
break;
}
}
diff --git a/sojorn_app/lib/utils/link_handler.dart b/sojorn_app/lib/utils/link_handler.dart
index fb158f5..26b9e14 100644
--- a/sojorn_app/lib/utils/link_handler.dart
+++ b/sojorn_app/lib/utils/link_handler.dart
@@ -28,7 +28,7 @@ class LinkHandler {
Uri? uri = Uri.tryParse(url.replaceFirst('sojorn://', 'sojorn://'));
// Normalize to https for query parsing if needed
uri ??=
- Uri.tryParse(url.replaceFirst('sojorn://', 'https://gosojorn.com/'));
+ Uri.tryParse(url.replaceFirst('sojorn://', 'https://sojorn.net/'));
final latParam = uri?.queryParameters['lat'];
final longParam = uri?.queryParameters['long'];
diff --git a/sojorn_app/lib/utils/url_launcher_helper.dart b/sojorn_app/lib/utils/url_launcher_helper.dart
index 5f25fd9..fabb03c 100644
--- a/sojorn_app/lib/utils/url_launcher_helper.dart
+++ b/sojorn_app/lib/utils/url_launcher_helper.dart
@@ -7,7 +7,7 @@ class UrlLauncherHelper {
// List of known safe domains
static const List _safeDomains = [
'mp.ls', 'www.mp.ls', 'patrick.mp.ls'
- 'gosojorn.com', 'www.gosojorn.com'
+ 'sojorn.net', 'www.sojorn.net'
'youtube.com', 'www.youtube.com', 'youtu.be',
'instagram.com', 'www.instagram.com',
'twitter.com', 'www.twitter.com', 'x.com', 'www.x.com',
diff --git a/sojorn_app/lib/widgets/media/signed_media_image.dart b/sojorn_app/lib/widgets/media/signed_media_image.dart
index c8becda..0b1a9ad 100644
--- a/sojorn_app/lib/widgets/media/signed_media_image.dart
+++ b/sojorn_app/lib/widgets/media/signed_media_image.dart
@@ -70,12 +70,12 @@ class _SignedMediaImageState extends ConsumerState {
final host = uri.host.toLowerCase();
// Custom domain URLs are public and directly accessible - no signing needed
- if (host == 'img.gosojorn.com' || host == 'quips.gosojorn.com') {
+ if (host == 'img.sojorn.net' || host == 'quips.sojorn.net') {
return false;
}
- // Legacy: media.gosojorn.com might need signing depending on setup
- if (host == 'media.gosojorn.com') {
+ // Legacy: media.sojorn.net might need signing depending on setup
+ if (host == 'media.sojorn.net') {
return true;
}
diff --git a/sojorn_app/lib/widgets/post/post_actions.dart b/sojorn_app/lib/widgets/post/post_actions.dart
index 95b3fcb..f5f36b4 100644
--- a/sojorn_app/lib/widgets/post/post_actions.dart
+++ b/sojorn_app/lib/widgets/post/post_actions.dart
@@ -278,11 +278,13 @@ class _PostActionsState extends ConsumerState {
if (allowChain)
ElevatedButton.icon(
onPressed: widget.onChain,
- icon: const Icon(Icons.reply, size: 18),
- label: const Text('Reply'),
+ icon: Icon(Icons.reply, size: 18, color: AppTheme.navyBlue),
+ label: Text('Reply', style: TextStyle(color: AppTheme.navyBlue, fontWeight: FontWeight.w600)),
style: ElevatedButton.styleFrom(
- backgroundColor: AppTheme.brightNavy,
- foregroundColor: Colors.white,
+ backgroundColor: AppTheme.navyBlue.withValues(alpha: 0.05),
+ foregroundColor: AppTheme.navyBlue,
+ elevation: 0,
+ shadowColor: Colors.transparent,
minimumSize: const Size(0, 44),
padding: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
diff --git a/sojorn_app/lib/widgets/post/post_header.dart b/sojorn_app/lib/widgets/post/post_header.dart
index 0a3efe9..a36c004 100644
--- a/sojorn_app/lib/widgets/post/post_header.dart
+++ b/sojorn_app/lib/widgets/post/post_header.dart
@@ -14,7 +14,7 @@ import '../../routes/app_routes.dart';
String _resolveAvatarUrl(String? url) {
if (url == null || url.isEmpty) return '';
if (url.startsWith('http://') || url.startsWith('https://')) return url;
- return 'https://img.gosojorn.com/${url.replaceFirst(RegExp('^/'), '')}';
+ return 'https://img.sojorn.net/${url.replaceFirst(RegExp('^/'), '')}';
}
/// Post header with author info and timestamp.
diff --git a/sojorn_app/lib/widgets/reactions/reactions_display.dart b/sojorn_app/lib/widgets/reactions/reactions_display.dart
index bfc4080..8a5a88f 100644
--- a/sojorn_app/lib/widgets/reactions/reactions_display.dart
+++ b/sojorn_app/lib/widgets/reactions/reactions_display.dart
@@ -54,11 +54,11 @@ class ReactionsDisplay extends StatelessWidget {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
- if (onAddReaction != null) ...[
- _ReactionAddButton(onTap: onAddReaction!),
- if (reactionCounts.isNotEmpty) const SizedBox(width: 8),
- ],
if (reactionCounts.isNotEmpty) _buildTopReactionChip(),
+ if (onAddReaction != null) ...[
+ if (reactionCounts.isNotEmpty) const SizedBox(width: 8),
+ _ReactionAddButton(onTap: onAddReaction!),
+ ],
],
);
}
@@ -95,6 +95,8 @@ class ReactionsDisplay extends StatelessWidget {
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
+ if (onAddReaction != null)
+ _ReactionAddButton(onTap: onAddReaction!),
...sortedEntries.map((entry) {
return _ReactionChip(
reactionId: entry.key,
@@ -105,8 +107,6 @@ class ReactionsDisplay extends StatelessWidget {
onLongPress: onAddReaction,
);
}),
- if (onAddReaction != null)
- _ReactionAddButton(onTap: onAddReaction!),
],
),
);
diff --git a/sojorn_app/run_chrome.bat b/sojorn_app/run_chrome.bat
index a64b6be..ec96a6d 100644
--- a/sojorn_app/run_chrome.bat
+++ b/sojorn_app/run_chrome.bat
@@ -5,4 +5,4 @@ echo Starting Sojorn on Chrome...
echo.
flutter run -d chrome ^
- --dart-define=API_BASE_URL=https://api.gosojorn.com/api/v1
+ --dart-define=API_BASE_URL=https://api.sojorn.net/api/v1
diff --git a/sojorn_app/run_dev.bat b/sojorn_app/run_dev.bat
index 9659e5b..edb35e4 100644
--- a/sojorn_app/run_dev.bat
+++ b/sojorn_app/run_dev.bat
@@ -5,4 +5,4 @@ echo Starting Sojorn in development mode...
echo.
flutter run ^
- --dart-define=API_BASE_URL=https://api.gosojorn.com/api/v1
+ --dart-define=API_BASE_URL=https://api.sojorn.net/api/v1
diff --git a/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md b/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md
index 0b215d1..8e1962b 100644
--- a/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md
+++ b/sojorn_docs/BACKEND_MIGRATION_COMPREHENSIVE.md
@@ -197,7 +197,7 @@ for _, origin := range allowedOrigins {
### Zero Downtime Approach
1. **Parallel Run**: Both Supabase and Go VPS running simultaneously
-2. **DNS Update**: Point `api.gosojorn.com` to new VPS IP
+2. **DNS Update**: Point `api.sojorn.net` to new VPS IP
3. **TTL Management**: Set DNS TTL to 300s before cutover
4. **Monitoring**: Real-time log monitoring for errors
@@ -212,7 +212,7 @@ for _, origin := range allowedOrigins {
**DNS Switch:**
```bash
-# Update A record for api.gosojorn.com
+# Update A record for api.sojorn.net
# Monitor propagation
# Watch error rates
```
@@ -223,7 +223,7 @@ for _, origin := range allowedOrigins {
journalctl -u sojorn-api -f
# Check error rates
-curl -s https://api.gosojorn.com/health
+curl -s https://api.sojorn.net/health
# Validate data integrity
sudo -u postgres psql sojorn -c "SELECT COUNT(*) FROM users;"
@@ -329,7 +329,7 @@ sudo -u postgres psql sojorn -c "SELECT COUNT(*) FROM users;"
### Emergency Rollback Procedure
-1. **DNS Reversion**: Point `api.gosojorn.com` back to Supabase
+1. **DNS Reversion**: Point `api.sojorn.net` back to Supabase
2. **Data Sync**: Restore any new data from Go backend to Supabase
3. **Service Restart**: Restart Supabase Edge Functions
4. **Client Update**: Update Flutter app configuration if needed
diff --git a/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md b/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md
index 2565377..8f23a40 100644
--- a/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md
+++ b/sojorn_docs/DEPLOYMENT_COMPREHENSIVE.md
@@ -4,6 +4,8 @@
This guide consolidates all deployment, operations, and maintenance documentation for the Sojorn platform, covering infrastructure setup, deployment procedures, monitoring, and operational best practices.
+All code updates need to be made locally, synced to git, pulled to the server and deployed. Do not directly edit files on the server.
+
---
## Infrastructure Setup
@@ -136,19 +138,19 @@ FIREBASE_CREDENTIALS_FILE=/opt/sojorn/firebase-service-account.json
FIREBASE_WEB_VAPID_KEY=BNxS7_your_vapid_key_here
# Storage
-R2_ACCOUNT_ID=your-r2-account-id
-R2_ACCESS_KEY_ID=your-access-key
-R2_SECRET_ACCESS_KEY=your-secret-key
-R2_BUCKET_NAME=sojorn-uploads
-R2_PUBLIC_BASE_URL=https://pub-xxxxx.r2.dev
-R2_IMG_DOMAIN=img.sojorn.com
-R2_VID_DOMAIN=vid.sojorn.com
+SENDER_API_TOKEN=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIxIiwianRpIjoiNT>
+R2_ACCOUNT_ID=7041ca6e0f40307190dc2e65e2fb5e0f
+R2_PUBLIC_BASE_URL=http://api.sojorn.net:8080/uploads
+R2_IMG_DOMAIN=img.sojorn.net
+R2_VID_DOMAIN=quips.sojorn.net
+R2_API_TOKEN=oR7Vk0Realtx0D6SAGMuYA8pXopSoCYKv8t3JEuk
# Email (Optional)
-SMTP_HOST=smtp.gmail.com
+# Email / SendPulse
+SMTP_HOST=smtp-pulse.com
SMTP_PORT=587
-SMTP_USER=noreply@sojorn.com
-SMTP_PASS=your-app-password
+SMTP_USER=patrickbritton3@gmail.com
+SMTP_PASS=8s4jQBnAFTCXPNM
# Logging
LOG_LEVEL=info
@@ -245,7 +247,7 @@ Create `/etc/nginx/sites-available/sojorn-api`:
```nginx
server {
listen 80;
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
# Redirect to HTTPS
return 301 https://$server_name$request_uri;
@@ -253,11 +255,11 @@ server {
server {
listen 443 ssl http2;
- server_name api.gosojorn.com;
+ server_name api.sojorn.net;
# SSL Configuration
- ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
@@ -346,7 +348,7 @@ sudo systemctl reload nginx
sudo apt install certbot python3-certbot-nginx
# Obtain certificate
-sudo certbot --nginx -d api.gosojorn.com
+sudo certbot --nginx -d api.sojorn.net
# Test renewal
sudo certbot renew --dry-run
@@ -556,7 +558,7 @@ psql -h localhost -U sojorn_user -d sojorn
sudo systemctl start sojorn-api
# Verify recovery
-curl -f https://api.gosojorn.com/health
+curl -f https://api.sojorn.net/health
```
#### File Recovery
@@ -939,7 +941,7 @@ sudo systemctl reload nginx
#### Problem: SSL handshake failed
```bash
# Test SSL configuration
-openssl s_client -connect api.gosojorn.com:443
+openssl s_client -connect api.sojorn.net:443
# Check Nginx configuration
sudo nginx -t
@@ -1003,7 +1005,7 @@ sudo systemctl reload nginx
sudo systemctl restart postgresql
# Verify recovery
- curl -f https://api.gosojorn.com/health
+ curl -f https://api.sojorn.net/health
```
3. **Communication**
diff --git a/sojorn_docs/MIGRATION_STEP_BY_STEP.txt b/sojorn_docs/MIGRATION_STEP_BY_STEP.txt
new file mode 100644
index 0000000..5232f9d
--- /dev/null
+++ b/sojorn_docs/MIGRATION_STEP_BY_STEP.txt
@@ -0,0 +1,95 @@
+MIGRATION COMMANDS CHEAT SHEET
+============================
+
+STEP 0: EDIT LOCAL SYNC GLOBALLY
+
+The first directive to to edit locally here, keep git updated fequently, then
+grab the updates files to our VPS server and then built and restart.
+
+------------
+
+STEP 1: FLUTTER WEB DEPLOY (If applicable)
+------------------------------------------
+Ensure your flutter web build is updated and copied to /var/www/sojorn
+flutter build web --release
+scp -r build/web/* user@server:/var/www/sojorn/
+
+STEP 2: NGINX CONFIGURATION
+---------------------------
+# 1. SSH into your VPS
+ssh ...
+
+# 2. Copy the new configs (upload them first or copy-paste)
+# Assuming you uploaded sojorn_net.conf and legacy_redirect.conf to /tmp/
+
+sudo cp /tmp/sojorn_net.conf /etc/nginx/sites-available/sojorn_net.conf
+sudo cp /tmp/legacy_redirect.conf /etc/nginx/sites-available/legacy_redirect.conf
+
+# 3. Enable new sites
+sudo ln -s /etc/nginx/sites-available/sojorn_net.conf /etc/nginx/sites-enabled/
+sudo ln -s /etc/nginx/sites-available/legacy_redirect.conf /etc/nginx/sites-enabled/
+
+# 4. Disable old site (to avoid conflicts with the new legacy_redirect which claims the same domains)
+# Check existing enabled sites
+ls -l /etc/nginx/sites-enabled/
+# Remove the old link (e.g., sojorn.conf or default)
+sudo rm /etc/nginx/sites-enabled/sojorn.conf
+# (Don't delete the actual file in sites-available, just the symlink)
+
+# 5. Test Configuration (This might fail on SSL paths if legacy certs are missing, but they should be there)
+sudo nginx -t
+
+# 6. Reload Nginx (Users will briefly see unencrypted or default page for new domain until Certbot runs)
+sudo systemctl reload nginx
+
+STEP 3: SSL CERTIFICATES (CERTBOT)
+----------------------------------
+# Generate fresh certs for the NEW domain.
+# --nginx plugin will automatically edit sojorn_net.conf to add SSL lines.
+
+sudo certbot --nginx -d sojorn.net -d www.sojorn.net -d api.sojorn.net
+
+# Follow the prompts. When asked about redirecting HTTP to HTTPS, choose "2: Redirect".
+
+STEP 4: BACKEND & ENV
+---------------------
+# Update your .env file on the server
+nano /opt/sojorn/.env
+# CHANGE:
+# CORS_ORIGINS=https://sojorn.net,https://api.sojorn.net,https://www.sojorn.net
+# R2_PUBLIC_BASE_URL=https://img.sojorn.net
+
+# Restart the backend service
+sudo systemctl restart sojorn-api
+
+STEP 5: VERIFICATION
+--------------------
+1. Visit https://sojorn.net -> Should show app.
+2. Visit https://sojorn.net -> Should redirect to https://sojorn.net.
+3. Check API: https://api.sojorn.net/api/v1/health (or similar).
+
+STEP 6: EXTERNAL SERVICES CHECKLIST
+-----------------------------------
+These items fall outside the codebase but are CRITICAL for the migration:
+
+1. CLOUDFLARE R2 (CORS)
+ - Go to Cloudflare Dashboard > R2 > [Your Bucket] > Settings > CORS Policy.
+ - Update allowed origins to include:
+ [ "https://sojorn.net", "https://www.sojorn.net", "http://localhost:*" ]
+ - If you don't do this, web image uploads will fail.
+
+2. FIREBASE CONSOLE (Auth & Messaging)
+ - Go to Firebase Console > Authentication > Settings > Authorized Domains.
+ - ADD: sojorn.net
+ - ADD: api.sojorn.net
+ - You can remove gosojorn.com later.
+
+3. GOOGLE CLOUD CONSOLE (If using Google Sign-In)
+ - APIs & Services > Credentials > OAuth 2.0 Client IDs.
+ - Add "https://sojorn.net" to Authorized JavaScript origins.
+ - Add "https://sojorn.net/auth.html" (or callback URI) to Authorized redirect URIs.
+
+4. APPLE DEVELOPER PORTAL (If using Sign in with Apple)
+ - Certificates, Identifiers & Profiles > Service IDs.
+ - Update the "Domains and Subdomains" list for your Service ID to include sojorn.net.
+
diff --git a/sojorn_docs/SOJORN_ARCHITECTURE.md b/sojorn_docs/SOJORN_ARCHITECTURE.md
index d544297..7b1dd6f 100644
--- a/sojorn_docs/SOJORN_ARCHITECTURE.md
+++ b/sojorn_docs/SOJORN_ARCHITECTURE.md
@@ -91,7 +91,7 @@ flutter run -d
### API Configuration
The app connects to the production API at:
```
-https://api.gosojorn.com (or http://194.238.28.122:8080)
+https://api.sojorn.net (or http://194.238.28.122:8080)
```
Configuration is in: `lib/config/api_config.dart`
@@ -140,7 +140,7 @@ R2_SECRET_KEY=...
| `internal/handlers/user_handler.go` | Profile & social endpoints |
| `internal/repository/post_repository.go` | Post database queries |
| `internal/repository/user_repository.go` | User database queries |
-
+aaa
---
## Server Deployment
diff --git a/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md b/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md
index 57a2a69..4e5f457 100644
--- a/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md
+++ b/sojorn_docs/TROUBLESHOOTING_COMPREHENSIVE.md
@@ -50,7 +50,7 @@ token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error)
**Verification**:
```bash
# Test JWT validation
-curl -H "Authorization: Bearer " https://api.gosojorn.com/health
+curl -H "Authorization: Bearer " https://api.sojorn.net/health
```
---
@@ -513,7 +513,7 @@ sudo certbot certificates
sudo nginx -t
# Check certificate expiry
-openssl x509 -in /etc/letsencrypt/live/api.gosojorn.com/cert.pem -text -noout | grep "Not After"
+openssl x509 -in /etc/letsencrypt/live/api.sojorn.net/cert.pem -text -noout | grep "Not After"
```
**Solutions**:
@@ -527,8 +527,8 @@ sudo systemctl reload nginx
#### 2. Fix Nginx SSL Config
```nginx
-ssl_certificate /etc/letsencrypt/live/api.gosojorn.com/fullchain.pem;
-ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem;
+ssl_certificate /etc/letsencrypt/live/api.sojorn.net/fullchain.pem;
+ssl_certificate_key /etc/letsencrypt/live/api.sojorn.net/privkey.pem;
```
### DNS Propagation Issues
@@ -541,11 +541,11 @@ ssl_certificate_key /etc/letsencrypt/live/api.gosojorn.com/privkey.pem;
**Diagnostics**:
```bash
# Check DNS resolution
-nslookup api.gosojorn.com
-dig api.gosojorn.com
+nslookup api.sojorn.net
+dig api.sojorn.net
# Check propagation
-for i in {1..10}; do echo "Attempt $i:"; dig api.gosojorn.com +short; sleep 30; done
+for i in {1..10}; do echo "Attempt $i:"; dig api.sojorn.net +short; sleep 30; done
```
**Solutions**:
@@ -553,11 +553,11 @@ for i in {1..10}; do echo "Attempt $i:"; dig api.gosojorn.com +short; sleep 30;
#### 1. Verify DNS Records
```bash
# Check A record
-dig api.gosojorn.com A
+dig api.sojorn.net A
# Check with multiple DNS servers
-dig @8.8.8.8 api.gosojorn.com
-dig @1.1.1.1 api.gosojorn.com
+dig @8.8.8.8 api.sojorn.net
+dig @1.1.1.1 api.sojorn.net
```
#### 2. Reduce TTL Before Changes
@@ -581,7 +581,7 @@ sudo -u postgres psql -c "SELECT count(*) FROM users;"
# Network
sudo netstat -tlnp | grep :8080
-curl -I https://api.gosojorn.com/health
+curl -I https://api.sojorn.net/health
# Logs
sudo tail -f /var/log/nginx/access.log
@@ -635,7 +635,7 @@ sudo journalctl -u sojorn-api --since "1 hour ago" | grep -i error
3. **Verify Health**:
```bash
- curl https://api.gosojorn.com/health
+ curl https://api.sojorn.net/health
```
### Database Recovery
diff --git a/sojorn_docs/deployment/VPS_SETUP_GUIDE.md b/sojorn_docs/deployment/VPS_SETUP_GUIDE.md
index 29da0b0..0b62552 100644
--- a/sojorn_docs/deployment/VPS_SETUP_GUIDE.md
+++ b/sojorn_docs/deployment/VPS_SETUP_GUIDE.md
@@ -10,7 +10,7 @@ Complete guide to deploy Sojorn Flutter Web app to your VPS with Nginx.
- VPS with Ubuntu 20.04/22.04 (or Debian-based distro)
- Root or sudo access
-- Domain name (gosojorn.com) pointed to your VPS IP
+- Domain name (sojorn.net) pointed to your VPS IP
- SSH access to your VPS
---
@@ -87,7 +87,7 @@ apt install certbot python3-certbot-nginx -y
**Important:** Make sure your domain DNS is already pointing to your VPS IP before running this.
```bash
-certbot --nginx -d gosojorn.com -d www.gosojorn.com
+certbot --nginx -d sojorn.net -d www.sojorn.net
```
Follow the prompts:
@@ -130,21 +130,21 @@ server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
- server_name www.gosojorn.com;
+ server_name www.sojorn.net;
- ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
- return 301 https://gosojorn.com$request_uri;
+ return 301 https://sojorn.net$request_uri;
}
# Main server block
server {
listen 80;
listen [::]:80;
- server_name gosojorn.com;
+ server_name sojorn.net;
# Redirect HTTP to HTTPS
return 301 https://$server_name$request_uri;
@@ -153,11 +153,11 @@ server {
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
- server_name gosojorn.com;
+ server_name sojorn.net;
# SSL Configuration
- ssl_certificate /etc/letsencrypt/live/gosojorn.com/fullchain.pem;
- ssl_certificate_key /etc/letsencrypt/live/gosojorn.com/privkey.pem;
+ ssl_certificate /etc/letsencrypt/live/sojorn.net/fullchain.pem;
+ ssl_certificate_key /etc/letsencrypt/live/sojorn.net/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
@@ -327,8 +327,8 @@ chmod -R 755 /var/www/sojorn
## Part 8: Test Your Deployment
-1. Visit `https://gosojorn.com` - you should see your app
-2. Test deep linking: `https://gosojorn.com/username` should route to a profile
+1. Visit `https://sojorn.net` - you should see your app
+2. Test deep linking: `https://sojorn.net/username` should route to a profile
3. Check SSL: Look for the padlock icon in the browser
---
@@ -508,10 +508,10 @@ certbot renew
You now have:
✅ Nginx web server installed and configured
✅ SSL certificate for HTTPS
-✅ Flutter Web app served at https://gosojorn.com
+✅ Flutter Web app served at https://sojorn.net
✅ Deep linking support for URLs like /username
✅ Gzip compression for better performance
✅ Proper security headers
✅ Caching for static assets
-Your app is now live and accessible at https://gosojorn.com! 🎉
+Your app is now live and accessible at https://sojorn.net! 🎉
diff --git a/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md b/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md
index 0212a34..da14a6b 100644
--- a/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md
+++ b/sojorn_docs/legacy/BACKEND_MIGRATION_RUNBOOK.md
@@ -43,7 +43,7 @@ This document outlines the step-by-step process for cutover from Supabase to the
## Phase 4: Cutover (Zero Downtime Strategy)
1. **Parallel Run**: Keep both Supabase and Go VPS running.
-2. **DNS Update**: Point your API subdomain (e.g., `api.gosojorn.com`) to the new VPS IP.
+2. **DNS Update**: Point your API subdomain (e.g., `api.sojorn.net`) to the new VPS IP.
3. **TTL Check**: Ensure DNS TTL is low (e.g., 300s) before starting.
4. **Monitor**: Watch logs for 4xx/5xx errors.
```bash
diff --git a/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md b/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md
index e00df85..b246b8a 100644
--- a/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md
+++ b/sojorn_docs/legacy/MIGRATION_VALIDATION_REPORT.md
@@ -9,7 +9,7 @@ The infrastructure for GoSojorn is **now fully functional and production-ready**
1. **CORS Resolved:** Fixed "Failed to fetch" errors by implementing dynamic origin matching (required for `AllowCredentials`).
2. **Schema Complete:** Manually applied missing Signal Protocol migrations (`000002_e2ee_chat.up.sql`).
3. **Data Success:** Expanded seeder now provides ~300 posts and ~70 users, satisfying load-test requirements.
-4. **Proxy Verified:** Nginx is correctly routing `api.gosojorn.com` to the Go service.
+4. **Proxy Verified:** Nginx is correctly routing `api.sojorn.net` to the Go service.
## Phase 1: Infrastructure & Environment Integrity
- **Service Health:** ✅
@@ -51,4 +51,4 @@ The infrastructure for GoSojorn is **now fully functional and production-ready**
- **Status:** Stress test threshold MET.
## Final Verdict
-The migration from Supabase to GoSojorn is **SUCCESSFUL**. The system is stable, the data is migrated/seeded, and the primary blocker (CORS) is removed. The Supabase instance can be safely paused after final client redirection to `api.gosojorn.com`.
+The migration from Supabase to GoSojorn is **SUCCESSFUL**. The system is stable, the data is migrated/seeded, and the primary blocker (CORS) is removed. The Supabase instance can be safely paused after final client redirection to `api.sojorn.net`.
diff --git a/sojorn_docs/reference/NEXT_STEPS.md b/sojorn_docs/reference/NEXT_STEPS.md
index 952233f..fb84f36 100644
--- a/sojorn_docs/reference/NEXT_STEPS.md
+++ b/sojorn_docs/reference/NEXT_STEPS.md
@@ -6,8 +6,8 @@ All backend configuration is done. Images should now work properly!
### What Was Configured:
-1. ✅ **Custom Domain Connected**: `media.gosojorn.com` → R2 bucket `sojorn-media`
-2. ✅ **Environment Variable Set**: `R2_PUBLIC_URL=https://media.gosojorn.com`
+1. ✅ **Custom Domain Connected**: `media.sojorn.net` → R2 bucket `sojorn-media`
+2. ✅ **Environment Variable Set**: `R2_PUBLIC_URL=https://media.sojorn.net`
3. ✅ **Edge Function Deployed**: Updated `upload-image` function using custom domain
4. ✅ **DNS Verified**: Domain resolving to Cloudflare CDN
5. ✅ **API Queries Fixed**: All post queries include `image_url` field
@@ -28,7 +28,7 @@ In the app:
**Expected behavior**:
- Image uploads successfully
- Post appears in feed with image visible
-- Image URL format: `https://media.gosojorn.com/{uuid}.jpg`
+- Image URL format: `https://media.sojorn.net/{uuid}.jpg`
**If it works**: Images will now display everywhere (feed, profiles, chains) ✅
@@ -43,7 +43,7 @@ ORDER BY created_at DESC
LIMIT 5;
```
-Expected format: `https://media.gosojorn.com/[uuid].[ext]`
+Expected format: `https://media.sojorn.net/[uuid].[ext]`
## Troubleshooting
@@ -65,7 +65,7 @@ Look for:
After uploading an image, copy its URL from the database and test:
```bash
-curl -I https://media.gosojorn.com/[filename-from-db]
+curl -I https://media.sojorn.net/[filename-from-db]
```
Should return `HTTP/1.1 200 OK` or `HTTP/2 200`
@@ -129,7 +129,7 @@ Complete guides available:
| API Queries | ✅ Include `image_url` field |
| Flutter Model | ✅ Post model parses `image_url` |
| Widget Display | ✅ PostItem widget shows images |
-| Custom Domain | ✅ `media.gosojorn.com` connected |
+| Custom Domain | ✅ `media.sojorn.net` connected |
**Ready to test!** 🚀
diff --git a/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md b/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md
index fa7b57d..501abb8 100644
--- a/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md
+++ b/sojorn_docs/troubleshooting/image-upload-fix-2025-01-08.md
@@ -253,7 +253,7 @@ Available keys: [id, body, created_at, tone_label, allow_chain,
**Other feed response (has image_url):**
```
-DEBUG Post.fromJson: Found image_url in JSON: https://media.gosojorn.com/88a7cc72-...
+DEBUG Post.fromJson: Found image_url in JSON: https://media.sojorn.net/88a7cc72-...
Available keys: [id, body, author_id, category_id, tone_label, cis_score,
status, created_at, edited_at, deleted_at, allow_chain,
chain_parent_id, image_url, chain_parent, metrics, author]
@@ -271,9 +271,9 @@ I/flutter: PostMedia: post.imageUrl = null
### After Fix
```
-I/flutter: DEBUG Post.fromJson: Found image_url in JSON: https://media.gosojorn.com/88a7cc72-...
-I/flutter: PostMedia: post.imageUrl = https://media.gosojorn.com/88a7cc72-...
-I/flutter: PostMedia: SHOWING IMAGE for https://media.gosojorn.com/88a7cc72-...
+I/flutter: DEBUG Post.fromJson: Found image_url in JSON: https://media.sojorn.net/88a7cc72-...
+I/flutter: PostMedia: post.imageUrl = https://media.sojorn.net/88a7cc72-...
+I/flutter: PostMedia: SHOWING IMAGE for https://media.sojorn.net/88a7cc72-...
I/flutter: PostMedia: Image loading... 8899 / 275401
I/flutter: PostMedia: Image LOADED successfully
```
@@ -291,7 +291,7 @@ npx supabase functions deploy feed-sojorn feed-personal --no-verify-jwt
### Image Upload Flow (Already Working)
1. User selects image in `ComposeScreen`
2. Image uploaded via `ImageUploadService.uploadImage()` to Cloudflare R2
-3. Returns public URL: `https://media.gosojorn.com/{uuid}.jpg`
+3. Returns public URL: `https://media.sojorn.net/{uuid}.jpg`
4. URL sent to `publish-post` edge function
5. Saved to `posts.image_url` column
diff --git a/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md b/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md
index 38fc1ec..c266d8a 100644
--- a/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md
+++ b/sojorn_docs/troubleshooting/test_image_upload_2025-01-05.md
@@ -4,9 +4,9 @@
- **R2 Bucket**: `sojorn-media`
- **Account ID**: `7041ca6e0f40307190dc2e65e2fb5e0f`
-- **Custom Domain**: `media.gosojorn.com`
+- **Custom Domain**: `media.sojorn.net`
- **Upload URL**: `https://7041ca6e0f40307190dc2e65e2fb5e0f.r2.cloudflarestorage.com/sojorn-media`
-- **Public URL**: `https://media.gosojorn.com`
+- **Public URL**: `https://media.sojorn.net`
## Quick Test
@@ -15,11 +15,11 @@
Go to: https://dash.cloudflare.com → R2 → `sojorn-media` bucket → Settings
Under "Custom Domains", you should see:
-- ✅ `media.gosojorn.com` with status "Active"
+- ✅ `media.sojorn.net` with status "Active"
If not connected:
1. Click "Connect Domain"
-2. Enter: `media.gosojorn.com`
+2. Enter: `media.sojorn.net`
3. Wait 1-2 minutes for activation
### 2. Test Upload in App
@@ -47,14 +47,14 @@ ORDER BY created_at DESC
LIMIT 1;
```
-**Expected URL format**: `https://media.gosojorn.com/[uuid].[ext]`
+**Expected URL format**: `https://media.sojorn.net/[uuid].[ext]`
### 4. Test URL Directly
Copy the image_url from database and test in browser or curl:
```bash
-curl -I https://media.gosojorn.com/[filename-from-database]
+curl -I https://media.sojorn.net/[filename-from-database]
```
**Expected response**: `HTTP/2 200 OK`
@@ -91,7 +91,7 @@ Required secrets:
| Issue | Cause | Solution |
|-------|-------|----------|
| Upload fails with 401 | Invalid R2 credentials | Check R2_ACCESS_KEY and R2_SECRET_KEY |
-| Upload succeeds but image 404 | Domain not connected | Connect media.gosojorn.com to bucket |
+| Upload succeeds but image 404 | Domain not connected | Connect media.sojorn.net to bucket |
| "Missing R2_PUBLIC_URL" | Secret not set/propagated | Wait 2 minutes, redeploy function |
| Image loads slowly | Not cached | Normal for first load, subsequent loads cached |
@@ -100,7 +100,7 @@ Required secrets:
✅ **Upload Flow**:
1. User selects image → App processes/filters
2. App uploads to edge function → Edge function uploads to R2
-3. Edge function returns: `https://media.gosojorn.com/[uuid].jpg`
+3. Edge function returns: `https://media.sojorn.net/[uuid].jpg`
4. App saves post with image_url to database
5. Feed queries posts with image_url
6. PostItem widget displays image