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:
@@ -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}
|
||||
@@ -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 }
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user