finishes feed
This commit is contained in:
26
db/main.go
26
db/main.go
@@ -120,7 +120,7 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
||||
app.OnRecordUpdate("integrations").BindFunc(updateIntegrationHandler())
|
||||
app.OnRecordAfterUpdateSuccess("integrations").BindFunc(createUpdateIntegrationSuccessHandler())
|
||||
|
||||
app.OnRecordsListRequest("feed").BindFunc(listFeedHandler())
|
||||
app.OnRecordsListRequest("feed", "profile_feed").BindFunc(listFeedHandler())
|
||||
|
||||
app.OnRecordCreateRequest().BindFunc(sanitizeHTML())
|
||||
app.OnRecordUpdateRequest().BindFunc(sanitizeHTML())
|
||||
@@ -905,19 +905,35 @@ func listFeedHandler() func(e *core.RecordsListRequestEvent) error {
|
||||
for _, r := range e.Records {
|
||||
var item *core.Record
|
||||
var err error
|
||||
switch r.GetString("type") {
|
||||
|
||||
typ := r.GetString("type")
|
||||
typ = strings.Trim(typ, "\"")
|
||||
|
||||
itemId := r.GetString("item")
|
||||
itemId = strings.Trim(itemId, "\"")
|
||||
|
||||
switch typ {
|
||||
case string(util.TrailFeed):
|
||||
item, err = e.App.FindRecordById("trails", r.GetString("item"))
|
||||
item, err = e.App.FindRecordById("trails", itemId)
|
||||
case string(util.ListFeed):
|
||||
item, err = e.App.FindRecordById("lists", r.GetString("item"))
|
||||
item, err = e.App.FindRecordById("lists", itemId)
|
||||
case string(util.SummitLogFeed):
|
||||
item, err = e.App.FindRecordById("summit_logs", r.GetString("item"))
|
||||
item, err = e.App.FindRecordById("summit_logs", itemId)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if item == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
errs := e.App.ExpandRecord(item, []string{"author"}, nil)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to expand: %v", errs)
|
||||
}
|
||||
|
||||
r.MergeExpand(map[string]any{"item": item})
|
||||
}
|
||||
return e.Next()
|
||||
|
||||
106
db/migrations/1752321031_created_profile_feed.go
Normal file
106
db/migrations/1752321031_created_profile_feed.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1148540665",
|
||||
"maxSize": 1,
|
||||
"name": "actor",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3182418120",
|
||||
"maxSize": 1,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json521872670",
|
||||
"maxSize": 1,
|
||||
"name": "item",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2363381545",
|
||||
"maxSize": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2990389176",
|
||||
"maxSize": 1,
|
||||
"name": "created",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}
|
||||
],
|
||||
"id": "pbc_1973704172",
|
||||
"indexes": [],
|
||||
"listRule": "",
|
||||
"name": "profile_feed",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n author,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n author,\n id as item,\n \"list\" as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n author,\n id as item,\n \"trail\" as type,\n created\n FROM trails\n )\nORDER BY created desc;",
|
||||
"viewRule": null
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
})
|
||||
}
|
||||
57
db/migrations/1752324952_updated_profile_feed.go
Normal file
57
db/migrations/1752324952_updated_profile_feed.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n id as item,\n 'list' as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n id as item,\n 'trail' as type,\n created\n FROM trails\n )\nORDER BY created desc;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("json3182418120")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"viewQuery": "SELECT\n (ROW_NUMBER() OVER ()) as id,\n actor,\n author,\n item,\n type,\n created\nFROM\n (\n SELECT\n author as actor,\n author,\n id as item,\n \"list\" as type,\n created\n FROM\n lists\n UNION\n SELECT\n author as actor,\n author,\n id as item,\n \"trail\" as type,\n created\n FROM trails\n )\nORDER BY created desc;"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "json3182418120",
|
||||
"maxSize": 1,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
44
db/migrations/1752324970_updated_feed.go
Normal file
44
db/migrations/1752324970_updated_feed.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("relation3182418120")
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_72164123")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{
|
||||
"cascadeDelete": true,
|
||||
"collectionId": "pbc_1295301207",
|
||||
"hidden": false,
|
||||
"id": "relation3182418120",
|
||||
"maxSelect": 1,
|
||||
"minSelect": 0,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": true,
|
||||
"system": false,
|
||||
"type": "relation"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
216
db/migrations/1752330359_deleted_timeline.go
Normal file
216
db/migrations/1752330359_deleted_timeline.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_468398817")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Delete(collection)
|
||||
}, func(app core.App) error {
|
||||
jsonData := `{
|
||||
"createRule": null,
|
||||
"deleteRule": null,
|
||||
"fields": [
|
||||
{
|
||||
"autogeneratePattern": "",
|
||||
"hidden": false,
|
||||
"id": "text3208210256",
|
||||
"max": 0,
|
||||
"min": 0,
|
||||
"name": "id",
|
||||
"pattern": "^[a-z0-9]+$",
|
||||
"presentable": false,
|
||||
"primaryKey": true,
|
||||
"required": true,
|
||||
"system": true,
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2310347867",
|
||||
"maxSize": 1,
|
||||
"name": "trail_id",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3184124860",
|
||||
"maxSize": 1,
|
||||
"name": "trail_author_username",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2887874732",
|
||||
"maxSize": 1,
|
||||
"name": "trail_author_domain",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json113557190",
|
||||
"maxSize": 1,
|
||||
"name": "trail_iri",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2862495610",
|
||||
"maxSize": 1,
|
||||
"name": "date",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1579384326",
|
||||
"maxSize": 1,
|
||||
"name": "name",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json1843675174",
|
||||
"maxSize": 1,
|
||||
"name": "description",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3275261007",
|
||||
"maxSize": 1,
|
||||
"name": "gpx",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3182418120",
|
||||
"maxSize": 1,
|
||||
"name": "author",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json142008537",
|
||||
"maxSize": 1,
|
||||
"name": "photos",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json479369857",
|
||||
"maxSize": 1,
|
||||
"name": "distance",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2254405824",
|
||||
"maxSize": 1,
|
||||
"name": "duration",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3015100073",
|
||||
"maxSize": 1,
|
||||
"name": "elevation_gain",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json3171089056",
|
||||
"maxSize": 1,
|
||||
"name": "elevation_loss",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2990389176",
|
||||
"maxSize": 1,
|
||||
"name": "created",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
},
|
||||
{
|
||||
"hidden": false,
|
||||
"id": "json2363381545",
|
||||
"maxSize": 1,
|
||||
"name": "type",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}
|
||||
],
|
||||
"id": "pbc_468398817",
|
||||
"indexes": [],
|
||||
"listRule": "@collection.trails.id ?= trail_id && @collection.trails.public ?= true",
|
||||
"name": "timeline",
|
||||
"system": false,
|
||||
"type": "view",
|
||||
"updateRule": null,
|
||||
"viewQuery": "SELECT\n id,\n trail_id,\n trail_author_username,\n trail_author_domain,\n trail_iri,\n date,\n name,\n description,\n gpx,\n author,\n photos,\n distance,\n duration,\n elevation_gain,\n elevation_loss,\n created,\n type\nFROM\n (\n SELECT\n summit_logs.id,\n summit_logs.trail as trail_id,\n tapa.preferred_username as trail_author_username,\n tapa.domain as trail_author_domain,\n trails.iri as trail_iri,\n summit_logs.date,\n trails.name,\n text as description,\n summit_logs.gpx,\n sapa.iri as author,\n summit_logs.photos,\n summit_logs.distance,\n summit_logs.duration,\n summit_logs.elevation_gain,\n summit_logs.elevation_loss,\n summit_logs.created,\n \"summit_log\" as type\n FROM\n summit_logs\n JOIN trails ON summit_logs.trail = trails.id\n JOIN activitypub_actors sapa ON sapa.id = summit_logs.author\n JOIN activitypub_actors tapa ON tapa.id = trails.author\n UNION\n SELECT\n trails.id,\n trails.id as trail_id,\n activitypub_actors.preferred_username as trail_author_username,\n activitypub_actors.domain as trail_author_domain,\n trails.iri as trail_iri,\n date,\n trails.name,\n description,\n gpx,\n activitypub_actors.iri as author,\n photos,\n distance,\n duration,\n elevation_gain,\n elevation_loss,\n trails.created,\n \"trail\" as type\n FROM\n trails\n JOIN activitypub_actors ON activitypub_actors.id = trails.author\n )\nORDER BY\n created DESC;\n",
|
||||
"viewRule": "@collection.trails.id ?= trail_id && @collection.trails.public ?= true"
|
||||
}`
|
||||
|
||||
collection := &core.Collection{}
|
||||
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
40
db/migrations/1752330401_updated_profile_feed.go
Normal file
40
db/migrations/1752330401_updated_profile_feed.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": "actor.user.settings_via_user.privacy.account != 'private' || actor.user = @request.auth.id"
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("pbc_1973704172")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update collection data
|
||||
if err := json.Unmarshal([]byte(`{
|
||||
"listRule": ""
|
||||
}`), &collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
let isOpen = $state(false);
|
||||
|
||||
let dropdownElement: HTMLUListElement;
|
||||
let dropdownElement: HTMLUListElement | undefined = $state();
|
||||
let dropdownToggleElement: HTMLDivElement;
|
||||
|
||||
export async function toggleMenu(e: MouseEvent) {
|
||||
@@ -30,7 +30,7 @@
|
||||
|
||||
isOpen = !isOpen;
|
||||
|
||||
if (isOpen) {
|
||||
if (isOpen && dropdownElement) {
|
||||
await tick();
|
||||
|
||||
const toggleRect = dropdownToggleElement.getBoundingClientRect();
|
||||
@@ -81,7 +81,8 @@
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleItemClick(e: Event, item: { text: string; value: any }) {
|
||||
function handleItemClick(e: MouseEvent, item: { text: string; value: any }) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onchange?.(item);
|
||||
closeMenu();
|
||||
@@ -129,7 +130,7 @@
|
||||
<li
|
||||
class="menu-item flex items-center px-4 py-3 cursor-pointer hover:bg-menu-item-background-hover focus:bg-menu-item-background-focus transition-colors"
|
||||
role="presentation"
|
||||
onmousedown={(e) => handleItemClick(e, item)}
|
||||
onclick={(e) => handleItemClick(e, item)}
|
||||
>
|
||||
{#if item.icon}
|
||||
<i class="fa fa-{item.icon} mr-3"></i>
|
||||
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: $_("profile"), value: "profile", icon: "user" },
|
||||
{ text: $_("my-trails"), value: "trails", icon: "route" },
|
||||
|
||||
{ text: $_("settings"), value: "settings", icon: "cog" },
|
||||
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
|
||||
];
|
||||
@@ -97,6 +99,8 @@
|
||||
function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (item.value == "profile") {
|
||||
goto(`/profile/@${$currentUser?.username?.toLowerCase()}`);
|
||||
} else if (item.value == "trails") {
|
||||
goto(`/profile/@${$currentUser?.username?.toLowerCase()}/trails`);
|
||||
} else if (item.value == "logout") {
|
||||
logout();
|
||||
window.location.href = "/";
|
||||
@@ -185,7 +189,11 @@
|
||||
</div>
|
||||
</Drawer>
|
||||
|
||||
<nav class="flex justify-between items-center p-6 {page.url.pathname === '/' ? 'sticky top-0 z-10 bg-background' : ''}">
|
||||
<nav
|
||||
class="flex justify-between items-center p-6 {page.url.pathname === '/'
|
||||
? 'sticky top-0 z-10 bg-background'
|
||||
: ''}"
|
||||
>
|
||||
<a href="/">
|
||||
{#if $theme == "light"}
|
||||
<LogoText></LogoText>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { FeedItem } from "$lib/models/feed";
|
||||
import type { List } from "$lib/models/list";
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL, isVideoURL } from "$lib/util/file_util";
|
||||
import {
|
||||
@@ -10,34 +11,48 @@
|
||||
formatTimeSince,
|
||||
} from "$lib/util/format_util";
|
||||
import { _ } from "svelte-i18n";
|
||||
import TrailDropdown from "../trail/trail_dropdown.svelte";
|
||||
interface Props {
|
||||
feedItem: FeedItem;
|
||||
}
|
||||
|
||||
let { feedItem }: Props = $props();
|
||||
|
||||
let fullDescription = $state(false)
|
||||
let fullDescription = $state(false);
|
||||
|
||||
const timeSince = formatTimeSince(new Date(feedItem.created ?? ""));
|
||||
const timeSince = $derived(formatTimeSince(new Date(feedItem.created ?? "")));
|
||||
|
||||
const photos = (feedItem.expand.item as Trail).photos
|
||||
const photos = $derived((feedItem.expand.item as Trail).photos);
|
||||
const location = $derived((feedItem.expand.item as Trail).location);
|
||||
|
||||
const trails = $derived((feedItem.expand.item as List).trails);
|
||||
|
||||
const author = $derived(feedItem.expand.item.expand?.author);
|
||||
</script>
|
||||
|
||||
<div class="feed-card p-6 rounded-xl border border-input-border">
|
||||
<div class="feed-card px-6 py-4 rounded-xl border border-input-border">
|
||||
<p class="mb-2 text-gray-500 text-sm">
|
||||
{#if feedItem.type === "trail"}
|
||||
<i class="fa fa-route mr-2"></i>{$_("trail", { values: { n: 1 } })}
|
||||
{:else if feedItem.type === "list"}
|
||||
<i class="fa fa-layer-group mr-2"></i>{$_("list", {
|
||||
values: { n: 1 },
|
||||
})}
|
||||
{/if}
|
||||
</p>
|
||||
|
||||
<a href="/profile/{author?.preferred_username}@{author?.domain}">
|
||||
<div class="feed-card-header flex gap-x-4 items-start">
|
||||
<img
|
||||
class="rounded-full w-10 aspect-square overflow-hidden shrink-0"
|
||||
src={feedItem.expand.author?.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${feedItem.expand.author?.preferred_username}&backgroundType=gradientLinear`}
|
||||
src={author?.icon ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${author?.preferred_username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<div>
|
||||
<span class="font-semibold"
|
||||
>{feedItem.expand.author?.preferred_username}</span
|
||||
>
|
||||
<span class="font-semibold">{author?.preferred_username}</span>
|
||||
<p class="text-sm text-gray-500 mb-3">
|
||||
{feedItem.expand.author?.preferred_username}@{feedItem.expand
|
||||
.author?.domain}
|
||||
{author?.preferred_username}@{author?.domain}
|
||||
</p>
|
||||
</div>
|
||||
<div class="basis-full"></div>
|
||||
@@ -47,11 +62,24 @@
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<a
|
||||
class="block"
|
||||
href={(feedItem.type === "trail" ? "/trail/view/" : "/lists/") +
|
||||
`@${author?.preferred_username}@${author?.domain}/${feedItem.item}`}
|
||||
>
|
||||
<div class="feed-card-body">
|
||||
<h3 class="text-2xl font-semibold !mt-2">
|
||||
{feedItem.expand.item.name}
|
||||
</h3>
|
||||
<div class="flex flex-wrap mt-1 gap-x-4 gap-y-2 text-sm text-gray-500">
|
||||
{#if location}
|
||||
<h5 class="my-1">
|
||||
<i class="fa fa-location-dot mr-3"></i>{location}
|
||||
</h5>
|
||||
{/if}
|
||||
<div
|
||||
class="flex flex-wrap mt-1 gap-x-4 gap-y-2 text-sm text-gray-500 mb-2"
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatDistance(
|
||||
feedItem.expand.item.distance,
|
||||
@@ -68,16 +96,25 @@
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-arrow-trend-down mr-2"></i>{formatElevation(
|
||||
><i class="fa fa-arrow-trend-down mr-2"
|
||||
></i>{formatElevation(
|
||||
feedItem.expand.item.elevation_loss,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
{#if trails}
|
||||
<p class="text-sm text-gray-500">
|
||||
{trails.length}
|
||||
{$_("trail", {
|
||||
values: { n: trails.length },
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
{#if photos?.length}
|
||||
<div
|
||||
class="grid gap-[1px] {photos.length > 1
|
||||
? 'grid-cols-[8fr_5fr]'
|
||||
: 'grid-cols-1'} mt-6"
|
||||
: 'grid-cols-1'} mt-4"
|
||||
>
|
||||
{#each photos.slice(0, 3) as photo, i}
|
||||
{#if isVideoURL(photo)}
|
||||
@@ -97,8 +134,7 @@
|
||||
{:else}
|
||||
<img
|
||||
class="object-cover h-full max-h-80 w-full"
|
||||
class:row-span-2={i == 0 &&
|
||||
photos.length > 2}
|
||||
class:row-span-2={i == 0 && photos.length > 2}
|
||||
src={getFileURL(
|
||||
{
|
||||
collectionId: "trails",
|
||||
@@ -116,7 +152,10 @@
|
||||
<p class="text-sm whitespace-pre-wrap mt-6">
|
||||
{formatHTMLAsText(
|
||||
!fullDescription
|
||||
? feedItem.expand.item.description?.substring(0, 100)
|
||||
? feedItem.expand.item.description?.substring(
|
||||
0,
|
||||
100,
|
||||
)
|
||||
: feedItem.expand.item.description,
|
||||
)}
|
||||
{#if (feedItem.expand.item.description?.length ?? 0) > 100 && !fullDescription}
|
||||
@@ -134,4 +173,23 @@
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{#if feedItem.type == "trail"}
|
||||
<div class="feed-card-actions flex items-center justify-end mt-4">
|
||||
<TrailDropdown
|
||||
trails={new Set<Trail>([feedItem.expand.item as Trail])}
|
||||
mode="overview"
|
||||
>
|
||||
{#snippet toggle({ toggleMenu: openDropdown })}
|
||||
<button
|
||||
class="btn-icon"
|
||||
onclick={openDropdown}
|
||||
aria-label="Trail actions"
|
||||
type="button"
|
||||
><i class="fa fa-ellipsis-vertical"></i></button
|
||||
>
|
||||
{/snippet}</TrailDropdown
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -21,14 +21,17 @@
|
||||
import TrailExportModal from "./trail_export_modal.svelte";
|
||||
import TrailShareModal from "./trail_share_modal.svelte";
|
||||
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
interface Props {
|
||||
trails?: Set<Trail> | undefined;
|
||||
mode: "overview" | "map" | "list" | "multi-select";
|
||||
onconfirm?: (resetSelection?: boolean) => void;
|
||||
toggle?: Snippet<[any]>;
|
||||
onDelete?: () => void;
|
||||
onShare?: () => void;
|
||||
}
|
||||
|
||||
let { trails, mode, onconfirm }: Props = $props();
|
||||
let { trails, mode, toggle, onDelete, onShare }: Props = $props();
|
||||
|
||||
let confirmModal: ConfirmModal;
|
||||
let listSelectModal: ListSelectModal;
|
||||
@@ -302,7 +305,7 @@
|
||||
await doDeleteTrail(dTrail);
|
||||
}
|
||||
|
||||
onconfirm?.(true);
|
||||
onDelete?.();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,7 +318,7 @@
|
||||
}
|
||||
|
||||
async function handleShareUpdate() {
|
||||
onconfirm?.();
|
||||
onShare?.();
|
||||
}
|
||||
|
||||
async function handleListSelection(list: List) {
|
||||
@@ -390,9 +393,14 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dropdown items={dropdownItems()} onchange={(item) => handleDropdownClick(item)}
|
||||
>{#snippet children({ toggleMenu: openDropdown })}
|
||||
{#if mode == "multi-select"}
|
||||
<Dropdown
|
||||
items={dropdownItems()}
|
||||
onchange={(item) => handleDropdownClick(item)}
|
||||
>
|
||||
{#snippet children({ toggleMenu: openDropdown })}
|
||||
{#if toggle}{@render toggle({
|
||||
toggleMenu: openDropdown,
|
||||
})}{:else if mode == "multi-select"}
|
||||
<button
|
||||
aria-label="Open dropdown"
|
||||
class="btn-primary flex-shrink-0 !font-medium"
|
||||
@@ -400,7 +408,8 @@
|
||||
>
|
||||
<span
|
||||
>{trails?.size}
|
||||
{$_("selected")} <i class="fa fa-caret-down ml-1"></i></span
|
||||
{$_("selected")}
|
||||
<i class="fa fa-caret-down ml-1"></i></span
|
||||
>
|
||||
</button>
|
||||
{:else}
|
||||
|
||||
@@ -403,6 +403,10 @@
|
||||
<LikeButton {trail}></LikeButton>
|
||||
<TrailDropdown
|
||||
trails={new Set<Trail>([trail])}
|
||||
onDelete={() =>
|
||||
history.length
|
||||
? history.back()
|
||||
: goto("/trails")}
|
||||
{mode}
|
||||
></TrailDropdown>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import TrailTable from "./trail_table.svelte";
|
||||
import SkeletonCard from "../base/skeleton_card.svelte";
|
||||
import SkeletonListItem from "../base/skeleton_list_item.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { onMount, tick } from "svelte";
|
||||
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
|
||||
|
||||
interface Props {
|
||||
@@ -191,15 +191,13 @@
|
||||
else hoveredTrail = undefined;
|
||||
}
|
||||
|
||||
function handleTrailsEditDone(resetSelection: boolean = false) {
|
||||
async function handleTrailsEditDone(resetSelection: boolean = false) {
|
||||
if (resetSelection) {
|
||||
selection?.clear();
|
||||
hoveredTrail = undefined;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
await tick();
|
||||
onupdate?.(filter, selection);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
function handleMouseEnter(trail: Trail) {
|
||||
@@ -224,7 +222,8 @@
|
||||
<TrailDropdown
|
||||
trails={selection}
|
||||
mode={"multi-select"}
|
||||
onconfirm={handleTrailsEditDone}
|
||||
onDelete={() => handleTrailsEditDone(true)}
|
||||
onShare={() => handleTrailsEditDone(false)}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Muss mindestens {n} Zeichen lang sein",
|
||||
"must-be-at-most-n-characters-long": "Darf höchstens {n} Zeichen lang sein",
|
||||
"my-account": "Mein Konto",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "vor {n} Tagen",
|
||||
"n-hours-ago": "vor {n} Stunden",
|
||||
"n-minutes-ago": "vor {n} Minuten",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Must be at least {n} characters long",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "My Account",
|
||||
"my-trails": "My trails",
|
||||
"n-days-ago": "{n} days ago",
|
||||
"n-hours-ago": "{n} hours ago",
|
||||
"n-minutes-ago": "{n} minutes ago",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Tiene que tener por lo menos {n} caracteres",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "Mi cuenta",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "hace {n} días",
|
||||
"n-hours-ago": "hace {n} horas",
|
||||
"n-minutes-ago": "hace {n} minutos",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Must be at least {n} characters long",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "My Account",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} days ago",
|
||||
"n-hours-ago": "{n} hours ago",
|
||||
"n-minutes-ago": "{n} minutes ago",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Doit être composé d'au moins {n} caractères",
|
||||
"must-be-at-most-n-characters-long": "Doit être au maximum de {n} caractères",
|
||||
"my-account": "Mon profil",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "il y a {n} jours",
|
||||
"n-hours-ago": "il y a {n} heures",
|
||||
"n-minutes-ago": "il y a {n} minutes",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Legalább {n} karakter hosszúnak kell lennie",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "My Account",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} days ago",
|
||||
"n-hours-ago": "{n} hours ago",
|
||||
"n-minutes-ago": "{n} minutes ago",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Deve essere lungo almeno {n} caratteri",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "Il mio account",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} giorni fa",
|
||||
"n-hours-ago": "{n} ore fa",
|
||||
"n-minutes-ago": "{n} minuti fa",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Minimaal {n} tekens",
|
||||
"must-be-at-most-n-characters-long": "Mag maximaal {n} tekens lang zijn.",
|
||||
"my-account": "Mijn Account",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} dagen geleden",
|
||||
"n-hours-ago": "{n} uuren geleden",
|
||||
"n-minutes-ago": "{n} minuten geleden",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Długość musi wynosić przynajmniej {n} znaków",
|
||||
"must-be-at-most-n-characters-long": "Musi mieć długość co najwyżej {n} znaków",
|
||||
"my-account": "Moje konto",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} dni temu",
|
||||
"n-hours-ago": "{n} godzin temu",
|
||||
"n-minutes-ago": "{n} minut temu",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Deve ter pelo menos {n} caracteres",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "A minha conta",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} dias atrás",
|
||||
"n-hours-ago": "{n} horas atrás",
|
||||
"n-minutes-ago": "{n} minutos atrás",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "Минимум {n} символов",
|
||||
"must-be-at-most-n-characters-long": "Максимум {n} символов",
|
||||
"my-account": "Мой аккаунт",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} дней назад",
|
||||
"n-hours-ago": "{n} часов назад",
|
||||
"n-minutes-ago": "{n} минут назад",
|
||||
|
||||
@@ -218,6 +218,7 @@
|
||||
"must-be-at-least-n-characters-long": "长度至少 {n} 字符",
|
||||
"must-be-at-most-n-characters-long": "Must be at most {n} characters long",
|
||||
"my-account": "我的账户",
|
||||
"my-trails": "",
|
||||
"n-days-ago": "{n} 天前",
|
||||
"n-hours-ago": "{n} 小时前",
|
||||
"n-minutes-ago": "{n} 分钟前",
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
import type { Actor } from "./activitypub/actor";
|
||||
import type { List } from "./list";
|
||||
import type { SummitLog } from "./summit_log";
|
||||
import type { Trail } from "./trail";
|
||||
|
||||
interface FeedItem {
|
||||
id: string;
|
||||
actor: string;
|
||||
author: string;
|
||||
item: string;
|
||||
type: "trail" | "list" | "summit_log"
|
||||
expand: {
|
||||
actor?: Actor,
|
||||
author?: Actor,
|
||||
item: Trail | List
|
||||
}
|
||||
created: string;
|
||||
|
||||
}
|
||||
|
||||
export { type FeedItem }
|
||||
export { type FeedItem };
|
||||
|
||||
@@ -7,7 +7,6 @@ let feed: FeedItem[] = []
|
||||
|
||||
export async function feed_index(page: number, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/feed?` + new URLSearchParams({
|
||||
expand: "author",
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
sort: '-created'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FeedItem } from "$lib/models/feed";
|
||||
import type { ListFilter } from "$lib/models/list";
|
||||
import type { SummitLog, SummitLogFilter } from "$lib/models/summit_log";
|
||||
import type { TimelineItem } from "$lib/models/timeline";
|
||||
import { defaultTrailSearchAttributes, Trail, type TrailFilter, type TrailSearchResult } from "$lib/models/trail";
|
||||
import { APIError } from "$lib/util/api_util";
|
||||
import type { Hits } from "meilisearch";
|
||||
@@ -10,7 +10,7 @@ import type { ListSearchResult } from "./search_store";
|
||||
import { buildFilterText } from "./summit_log_store";
|
||||
import { searchResultToTrailList } from "./trail_store";
|
||||
|
||||
let timeline: TimelineItem[] = []
|
||||
let feed: FeedItem[] = []
|
||||
|
||||
|
||||
export async function profile_show(handle: string, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
@@ -58,8 +58,8 @@ export async function profile_lists_index(handle: string, filter: ListFilter, pa
|
||||
}
|
||||
|
||||
|
||||
export async function profile_timeline_index(handle: string, page: number, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/timeline?` + new URLSearchParams({
|
||||
export async function profile_feed_index(handle: string, page: number, perPage: number = 10, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||
let r = await f(`/api/v1/profile/${handle}/feed?` + new URLSearchParams({
|
||||
page: page.toString(),
|
||||
perPage: perPage.toString(),
|
||||
sort: '-created'
|
||||
@@ -71,13 +71,13 @@ export async function profile_timeline_index(handle: string, page: number, perPa
|
||||
throw new APIError(r.status, response.message, response.detail)
|
||||
}
|
||||
|
||||
const fetchedTimeline: ListResult<TimelineItem> = await r.json()
|
||||
const fetchedFeed: ListResult<FeedItem> = await r.json()
|
||||
|
||||
const result = page > 1 ? [...timeline, ...fetchedTimeline.items] : fetchedTimeline.items
|
||||
const result = page > 1 ? [...feed, ...fetchedFeed.items] : fetchedFeed.items
|
||||
|
||||
timeline = result;
|
||||
feed = result;
|
||||
|
||||
return { ...fetchedTimeline, items: result };
|
||||
return { ...fetchedFeed, items: result };
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ export enum Collection {
|
||||
list_share = "list_share",
|
||||
lists = "lists",
|
||||
notifications = "notifications",
|
||||
profile_feed = "profile_feed",
|
||||
settings = "settings",
|
||||
summit_logs = "summit_logs",
|
||||
trail_like = "trail_like",
|
||||
@@ -36,7 +37,6 @@ export enum Collection {
|
||||
trails = "trails",
|
||||
tags = "tags",
|
||||
waypoints = "waypoints",
|
||||
timeline = "timeline",
|
||||
trails_bounding_box = "trails_bounding_box",
|
||||
trails_filter = "trails_filter",
|
||||
users_anonymous = "users_anonymous",
|
||||
|
||||
@@ -139,11 +139,12 @@
|
||||
<svelte:window onscroll={onScroll} />
|
||||
|
||||
<section
|
||||
class="hero grid grid-cols-1 lg:grid-cols-2 md:px-8 md:gap-8"
|
||||
class="hero grid grid-cols-1 lg:grid-cols-2 md:px-8 gap-4 md:gap-8"
|
||||
style="min-height: calc(100vh - 112px)"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col justify-center gap-8 max-w-md mx-8 sm:mx-auto mt-0 lg:-mt-24 md:mt-24 max-h-screen md:sticky top-0"
|
||||
class="flex flex-col justify-center gap-8 max-w-md mx-8 sm:mx-auto mt-0 lg:sticky"
|
||||
style="max-height: calc(100vh - 112px); top: 112px;"
|
||||
>
|
||||
<h2 class="text-5xl sm:text-6xl md:text-7xl font-bold">
|
||||
{$_("welcome_to")} <span class="-tracking-[0.075em]">wanderer</span>
|
||||
@@ -172,16 +173,8 @@
|
||||
<EmptyStateFeed></EmptyStateFeed>
|
||||
{/if}
|
||||
{#each feed.items as f}
|
||||
{#if f.expand.item}
|
||||
<a
|
||||
class="block"
|
||||
href={(f.type === "trail"
|
||||
? "/trail/view/"
|
||||
: "/lists/") +
|
||||
`@${f.expand.author?.preferred_username}@${f.expand.author?.domain}/${f.item}`}
|
||||
>
|
||||
{#if f.expand?.item}
|
||||
<FeedCard feedItem={f}></FeedCard>
|
||||
</a>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
52
web/src/routes/api/v1/profile/[handle]/feed/+server.ts
Normal file
52
web/src/routes/api/v1/profile/[handle]/feed/+server.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
|
||||
import { type FeedItem } from '$lib/models/feed';
|
||||
import type { Trail } from '$lib/models/trail';
|
||||
import { Collection, handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import { ClientResponseError, type ListResult } from 'pocketbase';
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
const handle = event.params.handle;
|
||||
if (!handle) {
|
||||
return error(400, { message: "Bad request" })
|
||||
}
|
||||
|
||||
try {
|
||||
const { actor, error } = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
|
||||
const searchParams = Object.fromEntries(event.url.searchParams);
|
||||
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
||||
|
||||
let feed: ListResult<FeedItem>;
|
||||
if (actor.isLocal) {
|
||||
feed = await event.locals.pb.collection(Collection.profile_feed)
|
||||
.getList<FeedItem>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `actor='${actor.id}'` })
|
||||
} else {
|
||||
const origin = new URL(actor.iri).origin
|
||||
const feedURL = `${origin}/api/v1/profile/${actor.preferred_username}/feed?` + event.url.searchParams
|
||||
|
||||
const response = await event.fetch(feedURL, { method: 'GET' })
|
||||
if (!response.ok) {
|
||||
const errorResponse = await response.json()
|
||||
throw new ClientResponseError({ status: response.status, response: errorResponse });
|
||||
}
|
||||
feed = await response.json()
|
||||
|
||||
feed.items.forEach(f => {
|
||||
if (f.type == "trail") {
|
||||
const trail = f.expand.item as Trail
|
||||
trail.photos = trail.photos.map(p =>
|
||||
`${origin}/api/v1/files/trails/${f.item}/${p}`
|
||||
)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return json(feed)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return handleError(e)
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { Actor } from '$lib/models/activitypub/actor';
|
||||
import { RecordListOptionsSchema } from '$lib/models/api/base_schema';
|
||||
import { type TimelineItem } from '$lib/models/timeline';
|
||||
import { Collection, handleError } from '$lib/util/api_util';
|
||||
import { error, json, type RequestEvent } from '@sveltejs/kit';
|
||||
import { ClientResponseError, type ListResult } from 'pocketbase';
|
||||
|
||||
export async function GET(event: RequestEvent) {
|
||||
const handle = event.params.handle;
|
||||
if (!handle) {
|
||||
return error(400, { message: "Bad request" })
|
||||
}
|
||||
|
||||
try {
|
||||
const {actor, error} = await event.locals.pb.send(`/activitypub/actor?resource=acct:${handle}`, { method: "GET", fetch: event.fetch, });
|
||||
|
||||
const searchParams = Object.fromEntries(event.url.searchParams);
|
||||
const safeSearchParams = RecordListOptionsSchema.parse(searchParams);
|
||||
|
||||
let timeline: ListResult<TimelineItem>;
|
||||
if (actor.isLocal) {
|
||||
timeline = await event.locals.pb.collection(Collection.timeline)
|
||||
.getList<TimelineItem>(safeSearchParams.page, safeSearchParams.perPage, { ...safeSearchParams, filter: `author='${actor.iri}'` })
|
||||
} else {
|
||||
const origin = new URL(actor.iri).origin
|
||||
const timelineURL = `${origin}/api/v1/profile/${actor.preferred_username}/timeline?` + event.url.searchParams
|
||||
|
||||
const response = await event.fetch(timelineURL, { method: 'GET' })
|
||||
if (!response.ok) {
|
||||
const errorResponse = await response.json()
|
||||
throw new ClientResponseError({ status: response.status, response: errorResponse });
|
||||
}
|
||||
timeline = await response.json()
|
||||
|
||||
timeline.items.forEach(i => {
|
||||
i.photos = i. photos.map(p =>
|
||||
`${origin}/api/v1/files/${i.type}s/${i.id}/${p}`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return json(timeline)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return handleError(e)
|
||||
}
|
||||
}
|
||||
@@ -3,23 +3,28 @@
|
||||
import { page } from "$app/state";
|
||||
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 ActivityCard from "$lib/components/profile/activity_card.svelte";
|
||||
import FeedCard from "$lib/components/profile/feed_card.svelte";
|
||||
import type { FeedItem } from "$lib/models/feed.js";
|
||||
|
||||
import type { TimelineItem } from "$lib/models/timeline.js";
|
||||
import { profile_timeline_index } from "$lib/stores/profile_store.js";
|
||||
import { profile_feed_index } from "$lib/stores/profile_store.js";
|
||||
import { theme } from "$lib/stores/theme_store.js";
|
||||
import { getFileURL } from "$lib/util/file_util.js";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
let timeline = $state(data.timeline);
|
||||
$effect(() => {
|
||||
data.feed;
|
||||
feed = data.feed
|
||||
})
|
||||
|
||||
let feed = $state(data.feed);
|
||||
|
||||
let loading: boolean = false;
|
||||
|
||||
let pagination = $derived({
|
||||
page: timeline.page,
|
||||
totalPages: timeline.totalItems,
|
||||
page: feed.page,
|
||||
totalPages: feed.totalItems,
|
||||
});
|
||||
|
||||
async function onListScroll(e: Event) {
|
||||
@@ -37,19 +42,19 @@
|
||||
|
||||
async function loadNextPage() {
|
||||
pagination.page += 1;
|
||||
timeline = await profile_timeline_index(
|
||||
page.params.handle,
|
||||
pagination.page,
|
||||
);
|
||||
feed = await profile_feed_index(page.params.handle, pagination.page);
|
||||
}
|
||||
|
||||
function handleTimeLineItemClick(item: TimelineItem) {
|
||||
if (item.trail_iri.length) {
|
||||
function handleFeedItemClick(f: FeedItem) {
|
||||
const author = f.expand.item.expand?.author;
|
||||
if (f.type == "trail") {
|
||||
goto(
|
||||
`/trail/view/@${item.trail_author_username}@${item.trail_author_domain}/${item.trail_id}`,
|
||||
`/trail/view/@${author?.preferred_username}@${author?.domain}/${f.item}`,
|
||||
);
|
||||
} else {
|
||||
goto(`/trail/view/${page.params.handle}/${item.trail_id}`);
|
||||
goto(
|
||||
`/lists/@${author?.preferred_username}@${author?.domain}/${f.item}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -130,24 +135,24 @@
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xl font-semibold">Timeline</h4>
|
||||
{#if !timeline.items?.length && data.isOwnProfile}
|
||||
{#if !feed.items?.length && data.isOwnProfile}
|
||||
<a class="btn-primary inline-block" href="/trails/edit/new"
|
||||
>+ {$_("new-trail")}</a
|
||||
>
|
||||
{:else if !timeline.items?.length}
|
||||
{:else if !feed.items?.length}
|
||||
<p class="w-full text-center text-gray-500 text-sm">
|
||||
{$_("empty-activities", {
|
||||
values: { username: data.profile.preferredUsername },
|
||||
})}
|
||||
</p>
|
||||
{/if}
|
||||
{#each timeline.items as item}
|
||||
{#each feed.items as item}
|
||||
<div
|
||||
class="py-1 cursor-pointer"
|
||||
role="presentation"
|
||||
onclick={() => handleTimeLineItemClick(item)}
|
||||
onclick={() => handleFeedItemClick(item)}
|
||||
>
|
||||
<ActivityCard activity={item} actor={data.actor}></ActivityCard>
|
||||
<FeedCard feedItem={item}></FeedCard>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ListFilter } from "$lib/models/list";
|
||||
import { profile_lists_index, profile_timeline_index } from "$lib/stores/profile_store";
|
||||
import { profile_lists_index, profile_feed_index } from "$lib/stores/profile_store";
|
||||
import { error, type Load } from "@sveltejs/kit";
|
||||
|
||||
export const load: Load = async ({ params, fetch, parent }) => {
|
||||
@@ -19,10 +19,10 @@ export const load: Load = async ({ params, fetch, parent }) => {
|
||||
|
||||
try {
|
||||
const lists = await profile_lists_index(params.handle, filter, 1, 6, fetch)
|
||||
const timeline = await profile_timeline_index(params.handle, 1, 10, fetch);
|
||||
return { lists: lists.items, timeline }
|
||||
const feed = await profile_feed_index(params.handle, 1, 10, fetch);
|
||||
return { lists: lists.items, feed }
|
||||
} catch(e) {
|
||||
return {lists: [], timeline: {items: [], page: 1, perPage: 1, totalItems: 0, totalPages: 0}}
|
||||
return {lists: [], feed: {items: [], page: 1, perPage: 1, totalItems: 0, totalPages: 0}}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user