Federation (#327)

* initial commit federation

* more federation

* more federation

* more federation

* completes follow, accept, undo

* add trail create activity

* process trail create activity

* more trail create activity

* adds missing endpoints

* adds update and delete activity

* adds comment activities

* adds activities back

* adds summit logs activities

* deletes follow counts table

* fixes migrations

* fixes migrations

* fixes migrations

* fixes migrations

* adds remote profiles

* ctd

* ctd

* we are getting closer...

* adds public summit logs

* fixes federated trails in lists

* adds remote lists

* adds list activites

* fixes lists

* adds notifications

* adds iri redirect

* fixes comments and summitlogs

* fixes follows and profiles

* adds asynchronous send

* fixes list search

* adds encryption key

* bug fixes

* fixes activity signing

* removes custon activity object types

* adds html editor

* fixes html editor

* fixes display issues on mastodon

* adds federated sharing

* finishes announcements

* fixes small summit log issues

* adds trail likes

* finalizes likes

* fixes images for komoot

* add disable federation option

* adds private profiles

* updates docs

* adds federated comments

* adds trail and comments actvitiypub routes

* adds mentions to editor

* adds mentions to trails, comments, summit logs

* updates theme

* updates theme

* update docs

* update docs

* updates docs

* fixes various frontend problems

* fixes activitypub follows api

* updates docs

* updates docs

---------

Co-authored-by: Christian Beutel <>
This commit is contained in:
Flomp
2025-06-21 10:09:22 +02:00
committed by GitHub
parent 742a6d9d30
commit 9276690d60
356 changed files with 57150 additions and 35470 deletions

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import ActorSearch from "$lib/components/actor_search.svelte";
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
import Search, {
type SearchItem,
@@ -16,16 +17,16 @@
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import TrailInfoPanel from "$lib/components/trail/trail_info_panel.svelte";
import TrailList from "$lib/components/trail/trail_list.svelte";
import UserSearch from "$lib/components/user_search.svelte";
import { List, type ListFilter } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import {
lists_delete,
lists_index,
lists_search_filter,
lists_show,
} from "$lib/stores/list_store";
import { fetchGPX } from "$lib/stores/trail_store";
import { trails_show } from "$lib/stores/trail_store";
import { currentUser } from "$lib/stores/user_store";
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util.js";
import * as M from "maplibre-gl";
import { onMount } from "svelte";
@@ -39,12 +40,12 @@
{ text: $_("creation-date"), value: "created" },
];
let pagination = {
page: data.lists.page,
const pagination: { page: number; totalPages: number } = $state({
page: 1,
totalPages: data.lists.totalPages,
};
});
let lists = $state(data.lists);
let lists: List[] = $state(data.lists.items);
let confirmModal: ConfirmModal;
let listShareModal: ListShareModal;
@@ -56,10 +57,12 @@
let markers: M.Marker[] = $state([]);
let showMap: boolean = true;
let selectedList: List | null = $state(null);
let selectedList: List | null = $state(
page.params.handle && page.params.id ? data.lists.items[0] : null,
);
let selectedTrail: Trail | null = $state(null);
let loading: boolean = $state(false);
let loading: boolean = $state(true);
let loadingNextPage: boolean = false;
let filterExpanded: boolean = $state(false);
@@ -67,24 +70,20 @@
let userQuery = $state("");
let selectedTrailIndex = $derived(
selectedTrail
? ((selectedList as List | null)?.expand?.trails?.indexOf(selectedTrail) ?? null)
: null,
);
let selectedTrailIndex = $derived(selectedTrail ? 0 : null);
let selectedTrailWaypoints = $derived(
(selectedTrail as Trail | null)?.expand?.waypoints,
);
onMount(() => {
if (page.url.searchParams.get("list")) {
setCurrentList(data.lists.items[0]);
if (page.params.handle && page.params.id) {
// setCurrentList(data.lists[0]);
// only the requested list has been loaded at this point
// load all lists the next time the user presses the back button
loadAllListsOnNextBack = true;
}
loading = false;
});
async function handleDropdownClick(item: DropdownItem) {
@@ -110,20 +109,12 @@
}
async function setCurrentList(item: List) {
if (
item.expand &&
item.expand.trails &&
item.expand.trails.length > 0
) {
for (const trail of item.expand.trails) {
const gpxData: string = await fetchGPX(trail);
if (!trail.expand) {
(trail as any).expand = {};
}
trail.expand!.gpx_data = gpxData;
}
}
selectedList = item;
const fullList = await lists_show(
item.id!,
handleFromRecordWithIRI(item),
fetch,
);
selectedList = fullList;
document.getElementById("list-container")?.scrollTo({ top: 0 });
}
@@ -144,8 +135,14 @@
}
}
function selectTrail(trail: Trail) {
selectedTrail = trail;
async function selectTrail(trail: Trail) {
const fullTrail = await trails_show(
trail.iri ? trail.iri.substring(trail.iri.length - 15) : trail.id!,
handleFromRecordWithIRI(trail),
true,
);
selectedTrail = fullTrail;
mapWithElevation?.unHighlightTrail(trail.id!);
window.scrollTo({ top: 0 });
}
@@ -177,7 +174,10 @@
async function loadNextPage() {
pagination.page += 1;
lists = await lists_index(filter, pagination.page);
const response = await lists_search_filter(filter, pagination.page);
lists = response.items;
pagination.page = response.page;
pagination.totalPages = response.totalPages;
}
async function updateFilter() {
@@ -194,7 +194,10 @@
}
pagination.page = 1;
lists = await lists_search_filter(filter, pagination.page);
const response = await lists_search_filter(filter, pagination.page);
lists = response.items;
pagination.page = response.page;
pagination.totalPages = response.totalPages;
loading = false;
}
@@ -268,7 +271,7 @@
</div>
{#if filterExpanded}
<div
class="absolute bg-background z-10 shadow-lg rounded-b-xl w-full px-14"
class="absolute bg-background z-10 shadow-lg border-b border-input-border rounded-b-xl w-full px-14"
in:slide
out:slide
>
@@ -291,13 +294,13 @@
{#if $currentUser}
<hr class="my-4 border-separator" />
<UserSearch
<ActorSearch
onclick={setAuthorFilter}
onclear={clearAuthorFilter}
bind:value={userQuery}
clearAfterSelect={false}
label={$_("author")}
></UserSearch>
></ActorSearch>
<div class="flex items-center my-4">
<input
id="public-checkbox"
@@ -338,10 +341,10 @@
{#each { length: 3 } as _, index}
<SkeletonCard></SkeletonCard>
{/each}
{:else if lists.items.length == 0}
{:else if lists.length == 0}
<EmptyStateSearch width={356}></EmptyStateSearch>
{:else}
{#each lists.items as item, i}
{#each lists as item, i}
<div
class="list-list-item"
onclick={() => setCurrentList(item)}
@@ -361,14 +364,20 @@
onclick={(data) => selectTrail(data.trail)}
></ListPanel>
{:else if selectedList && selectedTrail}
<TrailInfoPanel initTrail={selectedTrail} mode="list" {markers}
<TrailInfoPanel
initTrail={selectedTrail}
mode="list"
{markers}
handle={handleFromRecordWithIRI(selectedTrail)}
></TrailInfoPanel>
{/if}
</div>
</div>
<div id="trail-map" class:hidden={!showMap}>
<MapWithElevationMaplibre
trails={selectedList?.expand?.trails ?? []}
trails={selectedTrail
? [selectedTrail]
: (selectedList?.expand?.trails ?? [])}
waypoints={selectedTrailWaypoints}
bind:map
bind:this={mapWithElevation}

View File

@@ -1,8 +1,8 @@
import { ExpandType, type List, type ListFilter } from "$lib/models/list";
import { lists_index, lists_show } from "$lib/stores/list_store";
import { browser } from "$app/environment";
import { type ListFilter } from "$lib/models/list";
import { lists_search_filter, lists_show } from "$lib/stores/list_store";
import { APIError } from "$lib/util/api_util";
import { error, type Load, type NumericRange } from "@sveltejs/kit";
import { type ListResult } from "pocketbase";
export const load: Load = async ({ params, fetch, url }) => {
const filter: ListFilter = {
@@ -14,12 +14,12 @@ export const load: Load = async ({ params, fetch, url }) => {
sortOrder: "+",
};
let lists: ListResult<List>;
if (url.searchParams.get("list")) {
let lists: Awaited<ReturnType<typeof lists_search_filter>>;
if (params.handle && params.id) {
try {
const list = await lists_show(url.searchParams.get("list") ?? "", fetch)
const list = await lists_show(params.id, params.handle, fetch)
lists = { items: [list], page: 1, perPage: 1, totalItems: 1, totalPages: 1 }
lists = { items: [list], page: 1, totalPages: 1, hits: [] }
} catch (e) {
if (e instanceof APIError) {
error(e.status as NumericRange<400, 599>, {
@@ -28,8 +28,10 @@ export const load: Load = async ({ params, fetch, url }) => {
}
throw e
}
} else if (browser) {
lists = await lists_search_filter(filter, 1, undefined, fetch);
} else {
lists = await lists_index(filter, 1, undefined, fetch);
lists = { items: [], page: 1, totalPages: 1, hits: [] }
}
return { lists, filter }

View File

@@ -6,20 +6,20 @@
import emptyStateTrailDark from "$lib/assets/svgs/empty_states/empty_state_trail_dark.svg";
import emptyStateTrailLight from "$lib/assets/svgs/empty_states/empty_state_trail_light.svg";
import Button from "$lib/components/base/button.svelte";
import Editor from "$lib/components/base/editor.svelte";
import Search, {
type SearchItem,
} from "$lib/components/base/search.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import Textarea from "$lib/components/base/textarea.svelte";
import Toggle from "$lib/components/base/toggle.svelte";
import ConfirmModal from "$lib/components/confirm_modal.svelte";
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
import { ListCreateSchema } from "$lib/models/api/list_schema.js";
import { ListShareCreateSchema } from "$lib/models/api/list_share_schema.js";
import { TrailCreateSchema } from "$lib/models/api/trail_schema.js";
import type { Trail } from "$lib/models/trail.js";
import type { Trail, TrailSearchResult } from "$lib/models/trail.js";
import { TrailShare } from "$lib/models/trail_share.js";
import { lists_create, lists_update } from "$lib/stores/list_store.js";
import { searchTrails } from "$lib/stores/search_store.js";
import { theme } from "$lib/stores/theme_store.js";
import { show_toast } from "$lib/stores/toast_store.svelte.js";
import {
@@ -37,10 +37,7 @@
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { z } from "zod";
import {
searchTrails,
type TrailSearchResult,
} from "$lib/stores/search_store.js";
import { onMount } from "svelte";
let { data } = $props();
@@ -58,13 +55,23 @@
let shareConfirmModal: ConfirmModal;
let publishConfirmModal: ConfirmModal;
let trailsOnMap: Trail[] = $state([]);
const ClientListCreateSchema = ListCreateSchema.extend({
_photos: z.array(z.instanceof(File)).optional(),
avatar: z.string().or(z.instanceof(File)).optional(),
expand: z
.object({
trails: z.array(TrailCreateSchema).optional(),
list_share_via_list: z.array(ListShareCreateSchema).optional(),
list_share_via_list: z
.array(
z.object({
actor: z.string().length(15),
list: z.string().length(15),
permission: z.enum(["view", "edit"]),
}),
)
.optional(),
})
.optional(),
});
@@ -90,6 +97,10 @@
},
});
onMount(() => {
trailsOnMap = [...(data.list.expand?.trails ?? [])];
});
async function checkPrerequisites() {
if (
(data.list?.public === false && $formData.public === true) ||
@@ -103,7 +114,6 @@
shareConfirmModal.openModal();
return false;
}
return true;
}
@@ -156,7 +166,7 @@
async function search(q: string) {
try {
const r = await searchTrails(q, {
filter: `author = ${$currentUser?.id} OR public = true`,
filter: `author = ${$currentUser?.actor} OR public = true`,
sort: ["name:desc"],
limit: 3,
});
@@ -170,19 +180,21 @@
description: `${t.location ?? "-"}`,
value: t.id,
icon: "route",
}));
}));
} catch (e) {
console.error(e);
}
}
async function handleSearchClick(item: SearchItem) {
const trail = await trails_show(item.value, true);
const trail = await trails_show(item.value, undefined, true);
$formData.trails?.push(trail.id!);
$formData.expand = {
trails: [...($formData.expand?.trails ?? []), trail],
list_share_via_list: $formData.expand?.list_share_via_list,
};
trailsOnMap = [...($formData.expand?.trails ?? [])];
}
function deleteTrail(trail: Trail) {
@@ -190,20 +202,24 @@
$formData.expand!.trails = $formData.expand!.trails!.filter(
(t) => t.id !== trail.id,
);
trailsOnMap = [...($formData.expand?.trails ?? [])];
}
async function findNewTrailShares() {
const usersWithAccess: string[] = [
const actorsWithAccess: string[] = [
$formData.author!,
...($formData.expand?.list_share_via_list ?? []).map((s) => s.user),
...($formData.expand?.list_share_via_list ?? []).map(
(s) => s.actor!,
),
];
for (const userId of usersWithAccess) {
for (const actorId of actorsWithAccess) {
const existingTrailShares = await trail_share_index({
user: userId,
actor: actorId,
});
for (const trail of $formData.expand?.trails ?? []) {
if (
trail.author == userId ||
trail.author == actorId ||
trail.author != $currentUser?.id
) {
continue;
@@ -212,7 +228,7 @@
(s) => s.trail == trail.id,
);
if (!trailShare) {
newShares.push(new TrailShare(userId, trail.id!, "view"));
newShares.push(new TrailShare(actorId, trail.id!, "view"));
}
}
}
@@ -228,7 +244,7 @@
async function publishTrails() {
for (const trail of $formData.expand?.trails ?? []) {
if (trail.author !== $currentUser?.id) {
if (trail.author !== $currentUser?.actor) {
continue;
}
const updatedTrail: Trail = { ...trail, public: true };
@@ -310,11 +326,12 @@
<TextField name="name" label={$_("name")} error={$errors.name}
></TextField>
<Textarea
name="description"
<Editor
extraClasses="min-h-24"
bind:value={$formData.description}
label={$_("description")}
error={$errors.description}
></Textarea>
></Editor>
<Toggle name="public" label={$_("public")}></Toggle>
<h3 class="text-xl font-semibold">
{$_("trail", { values: { n: 2 } })}
@@ -340,7 +357,8 @@
src={trail.photos.length
? getFileURL(
trail,
trail.photos[trail.thumbnail ?? 0],
trail.photos.at(trail.thumbnail ?? 0) ??
trail.photos[0],
)
: $theme === "light"
? emptyStateTrailLight
@@ -429,7 +447,7 @@
<div id="trail-map" class="max-h-full">
<MapWithElevationMaplibre
fitBounds="animate"
trails={$formData.expand?.trails ?? []}
trails={trailsOnMap}
bind:activeTrail={activeTrailIndex}
bind:this={map}
></MapWithElevationMaplibre>

View File

@@ -15,7 +15,7 @@ export const load: Load = async ({ params, fetch, data }) => {
return { list: list, previewUrl: "" }
} else {
try {
list = await lists_show(params.id, fetch);
list = await lists_show(params.id, undefined, fetch);
const previewURL = getFileURL(list, list.avatar);
return { list: list, previewUrl: previewURL }