diff --git a/AGENTS.md b/AGENTS.md index dfe0aa2..3f22037 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -632,6 +632,7 @@ Watch for: - believing `make warnings` covers `tools/snapshot.c`. It builds only `src/` and `tests/`, so a snapshot scenario that does not compile is caught by `make snapshots` alone; +- **Profile tabs.** A person's posts screen (`INDIGO_SEARCH_AUTHOR`) takes its tabs, filters and fetch from `wolfram/profile_tab.h`; do not write Indigo's own filter strings. `indigo_search.tab` holds the tab, the header box cycles it (`indigo_app_next_author_tab`), and likes is offered on the signed-in account only. - believing the host targets cover `src/atproto/session*.c`. Its Wolfram calls all sit behind `#if defined(__3DS__)`, so `make test`, `make warnings` and `make snapshots` compile none of them. A wrong Wolfram signature, a missing include or an undeclared static has passed all three and failed only the cross-build, five times now: `wf_display_facet`, a missing `` for `UINT_MAX`, `wf_agent_get_author_feed_typed`'s `filter` argument, `s_account` (a static that never existed; the agent's own handle comes from `wf_agent_get_handle`), and a missing `` for `wf_feedgen_get_feed_generators_typed`. Run `make` for the console before calling a session change done; - trusting `make warnings` to match the cross-build's diagnostics. The host sweep compiles at `-O2` but the `make test` binary is `-O1`, and the ARM diff --git a/CHANGELOG.md b/CHANGELOG.md index 907da45..c648506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ as the release notes. ## [Unreleased] +### Added + +- A person's posts screen has replies, media and (on your own account) likes tabs: tap the header box to move to the next one. The tabs, their filters and the fetch are Wolfram v0.36's `wolfram/profile_tab.h`. ([#67](https://github.com/ewanc26/indigo/pull/67)) + ## [0.10.0] - 2026-10-07 What has and has not been run, plainly: the host tests pass (4019 checks under AddressSanitizer and UBSan), all 148 snapshots render, and the 3DS build links in CI against Wolfram v0.35.0. The new image attachment (picker, alt text, upload) has been run on the host only, not on an emulator or a real 3DS, and the upload has never reached a server from Indigo. The code moved into smaller files in this release; its layout renders byte-identically to 0.9.0 but the worker thread and touch handling were not re-run on a console. diff --git a/docs/PARITY.md b/docs/PARITY.md index 44a240d..275b46e 100644 --- a/docs/PARITY.md +++ b/docs/PARITY.md @@ -39,7 +39,7 @@ An unknown pairing code (a restarted node forgets its pairings) used to be polle | Notifications, mark as seen | yes | implemented | emulator fetch; seen marking host only | | Profiles, follow, unfollow | yes | implemented | host | | Followers, following | yes | implemented, paged | host | -| Profile tabs | yes | partial: a person's posts only (`INDIGO_SEARCH_AUTHOR`) | host; no replies, media or likes tab (#56) | +| Profile tabs | yes | yes: posts, replies, media and likes (likes on the signed-in account only), cycled by tapping the header box on a person's posts (`INDIGO_SEARCH_AUTHOR`) | host tests; not run on a 3DS or Azahar | | Pinned posts | yes | implemented | host | | Avatars | yes | implemented | host, and Azahar: fetched as 128 px JPEG thumbnails, decoded and drawn on the timeline; not on hardware | | Images on posts, replies and quotes, with alt text | yes | implemented for viewing, behind an alt-text setting | host only | diff --git a/mk/host.mk b/mk/host.mk index f4db218..84c4e9f 100644 --- a/mk/host.mk +++ b/mk/host.mk @@ -26,7 +26,7 @@ endif HOST_CJSON_CFLAGS ?= $(shell pkg-config --cflags libcjson 2>/dev/null) HOST_CJSON_LIBS ?= $(shell pkg-config --libs libcjson 2>/dev/null || echo -lcjson) HOST_WOLFRAM_INC := -I$(WOLFRAM_ROOT)/include $(HOST_CJSON_CFLAGS) -HOST_WOLFRAM_FILES := src/agent/muted_words.c src/agent/moderation.c src/failure.c src/time.c src/cdn.c src/attach.c src/util.c src/update/ed25519.c \ +HOST_WOLFRAM_FILES := src/agent/muted_words.c src/agent/profile_tab.c src/agent/moderation.c src/failure.c src/time.c src/cdn.c src/attach.c src/util.c src/update/ed25519.c \ src/update/signature.c src/update/update_core.c HOST_WOLFRAM_CXX_FILES := cpp/wolfram/syntax.cpp HOST_WOLFRAM_OBJS := $(addprefix $(HOST_OUT)/wf_,$(subst /,_,$(HOST_WOLFRAM_FILES:.c=.o))) \ diff --git a/src/app/app.h b/src/app/app.h index 325c6e4..95a6cad 100644 --- a/src/app/app.h +++ b/src/app/app.h @@ -203,6 +203,8 @@ void indigo_app_search_failed(indigo_app *app, const char *message); void indigo_app_open_people(indigo_app *app, indigo_search_kind kind, const char *subject); /* One person's posts. Reuses the post list that post search fills. */ void indigo_app_open_author_posts(indigo_app *app, const char *actor); +int indigo_app_author_tab_after(const indigo_app *app); +void indigo_app_next_author_tab(indigo_app *app); /* The signed-in account's curated lists, then one list's members. Both reuse * the search screen; the members list reuses the actor rows. */ void indigo_app_open_lists(indigo_app *app); diff --git a/src/app/app_results.c b/src/app/app_results.c index ede2986..0c10370 100644 --- a/src/app/app_results.c +++ b/src/app/app_results.c @@ -2,6 +2,9 @@ #include "app/app_internal.h" +#include +#include + void indigo_app_begin_sign_in(indigo_app *app, const char *status) { @@ -183,18 +186,38 @@ indigo_app_open_people(indigo_app *app, indigo_search_kind kind, const char *sub app->request = INDIGO_REQUEST_PEOPLE; } -void -indigo_app_open_author_posts(indigo_app *app, const char *actor) -{ - if (!actor || !actor[0]) { - return; - } - app->screen = INDIGO_SCREEN_SEARCH; - app->search.kind = INDIGO_SEARCH_AUTHOR; - indigo_search_begin_page(&app->search, false); - indigo_copy_utf8(app->search.subject, sizeof app->search.subject, actor); - indigo_copy_utf8(app->request_actor, sizeof app->request_actor, actor); - app->request = INDIGO_REQUEST_AUTHOR_FEED; +static void begin_author_tab(indigo_app *app, const char *actor, int tab) { + app->screen = INDIGO_SCREEN_SEARCH; + app->search.kind = INDIGO_SEARCH_AUTHOR; + indigo_search_begin_page(&app->search, false); + app->search.tab = tab; + indigo_copy_utf8(app->search.subject, sizeof app->search.subject, actor); + indigo_copy_utf8(app->request_actor, sizeof app->request_actor, actor); + app->request = INDIGO_REQUEST_AUTHOR_FEED; +} + +void indigo_app_open_author_posts(indigo_app *app, const char *actor) { + if (actor && actor[0]) { + begin_author_tab(app, actor, WF_PROFILE_TAB_POSTS); + } +} + +/* The tab after the one on screen. The likes tab is only offered for the + * signed-in account, so the cycle skips it for anyone else. */ +int indigo_app_author_tab_after(const indigo_app *app) { + bool self = strcasecmp(app->search.subject, app->signin.account) == 0; + + return wf_profile_tab_next(app->search.tab, self); +} + +void indigo_app_next_author_tab(indigo_app *app) { + char actor[INDIGO_POST_NAME_MAX]; + + if (app->search.kind != INDIGO_SEARCH_AUTHOR || app->search.loading) { + return; + } + indigo_copy_utf8(actor, sizeof actor, app->request_actor); + begin_author_tab(app, actor, indigo_app_author_tab_after(app)); } void diff --git a/src/app/app_screens.c b/src/app/app_screens.c index 5d56544..219da73 100644 --- a/src/app/app_screens.c +++ b/src/app/app_screens.c @@ -343,6 +343,8 @@ indigo_app_update_search(indigo_app *app, const indigo_input *input) case INDIGO_ACTION_FIELD_QUERY: if (indigo_search_is_typed(s)) { edit_query(app); + } else if (s->kind == INDIGO_SEARCH_AUTHOR) { + indigo_app_next_author_tab(app); } break; case INDIGO_ACTION_AUTHOR: diff --git a/src/app/search.c b/src/app/search.c index 66d8e13..2e7bb1a 100644 --- a/src/app/search.c +++ b/src/app/search.c @@ -1,6 +1,7 @@ #include "app/search.h" #include +#include static void keep_visible(unsigned *scroll, unsigned selected, unsigned rows) @@ -158,7 +159,7 @@ indigo_search_title(const indigo_search *s) case INDIGO_SEARCH_POSTS: return "Post search"; case INDIGO_SEARCH_AUTHOR: - return "Posts"; + return wf_profile_tab_name(s->tab); case INDIGO_SEARCH_LISTS: return "Lists"; case INDIGO_SEARCH_LIST_MEMBERS: diff --git a/src/app/search.h b/src/app/search.h index be9f9ab..5681f4f 100644 --- a/src/app/search.h +++ b/src/app/search.h @@ -60,6 +60,8 @@ typedef struct { * around one person, so they carry the subject instead of a query. */ char query[INDIGO_SEARCH_QUERY_MAX]; indigo_search_kind kind; + /* The profile tab (wf_profile_tab) an INDIGO_SEARCH_AUTHOR list shows. */ + int tab; /* A handle for the people lists, a post's at:// URI for liked-by and * reposted-by, so it is sized for the longer of the two. */ char subject[INDIGO_POST_URI_MAX]; diff --git a/src/atproto/session.h b/src/atproto/session.h index da9dc50..776395d 100644 --- a/src/atproto/session.h +++ b/src/atproto/session.h @@ -140,7 +140,7 @@ bool indigo_session_submit_search(const char *query, bool paging); bool indigo_session_submit_post_search(const char *query, bool paging); /* One person's posts. Shares post search's result array and event, the way the * people lists share the actor search's. */ -bool indigo_session_submit_author_feed(const char *actor, bool paging); +bool indigo_session_submit_author_feed(const char *actor, int tab, bool paging); /* The signed-in account's curated lists, then one list's members. The members * reuse the actor search's result array and events, the way the people lists * do; the lists themselves have their own. */ diff --git a/src/atproto/session_internal.h b/src/atproto/session_internal.h index b7d248e..0f6b0dc 100644 --- a/src/atproto/session_internal.h +++ b/src/atproto/session_internal.h @@ -37,21 +37,22 @@ #if defined(__3DS__) && defined(WOLFRAM_3DS) #include <3ds.h> -#include #include #include #include #include #include -#include -#include #include #include #include +#include #include #include +#include +#include #include #include +#include typedef enum { JOB_NONE = 0, @@ -104,6 +105,8 @@ typedef struct { indigo_graph_action graph; indigo_search_kind people_kind; char actor[INDIGO_PROFILE_DID_MAX]; + /* JOB_AUTHOR_FEED's wf_profile_tab. */ + int tab; char list_uri[INDIGO_POST_URI_MAX]; /* JOB_FEED's target; the same shape as list_uri, kept separate so the two * jobs stay readable. */ diff --git a/src/atproto/session_read.c b/src/atproto/session_read.c index 4ecf942..9511bf0 100644 --- a/src/atproto/session_read.c +++ b/src/atproto/session_read.c @@ -218,12 +218,8 @@ indigo_session_do_author_feed(const indigo_job *j) return; } memset(&list, 0, sizeof list); - st = wf_agent_get_author_feed_typed(g_session.agent, j->actor, INDIGO_SEARCH_PAGE, cursor, - /* No filter: posts_with_replies would mix - * replies into a person's own posts, and - * replies_filter removes everything but - * them. The bare call is their posts. */ - NULL, &list); + st = wf_agent_get_profile_tab_typed(g_session.agent, j->actor, j->tab, + INDIGO_SEARCH_PAGE, cursor, &list); if (st != WF_OK) { indigo_session_publish_failure(&ev, "author feed", st); return; @@ -423,17 +419,17 @@ indigo_session_submit_post_search(const char *query, bool paging) return indigo_session_enqueue(&j); } -bool -indigo_session_submit_author_feed(const char *actor, bool paging) -{ - indigo_job j = {.kind = JOB_AUTHOR_FEED}; - - if (!actor || !actor[0]) { - return false; - } - j.paging = paging; - indigo_copy_utf8(j.actor, sizeof j.actor, actor); - return indigo_session_enqueue(&j); +bool indigo_session_submit_author_feed(const char *actor, int tab, + bool paging) { + indigo_job j = {.kind = JOB_AUTHOR_FEED}; + + if (!actor || !actor[0]) { + return false; + } + j.paging = paging; + j.tab = tab; + indigo_copy_utf8(j.actor, sizeof j.actor, actor); + return indigo_session_enqueue(&j); } bool diff --git a/src/atproto/session_stub.c b/src/atproto/session_stub.c index c9f073e..d6cb552 100644 --- a/src/atproto/session_stub.c +++ b/src/atproto/session_stub.c @@ -200,12 +200,12 @@ indigo_session_submit_post_search(const char *query, bool paging) return false; } -bool -indigo_session_submit_author_feed(const char *actor, bool paging) -{ - (void) actor; - (void) paging; - return false; +bool indigo_session_submit_author_feed(const char *actor, int tab, + bool paging) { + (void)actor; + (void)tab; + (void)paging; + return false; } bool diff --git a/src/main.c b/src/main.c index 1163fd5..7e15526 100644 --- a/src/main.c +++ b/src/main.c @@ -255,10 +255,11 @@ handle_requests(indigo_app *app) } break; case INDIGO_REQUEST_AUTHOR_FEED: - if (!indigo_session_submit_author_feed(app->request_actor, - app->search.cursor[0] != '\0')) { - indigo_app_search_failed(app, "Could not start the request."); - } + if (!indigo_session_submit_author_feed(app->request_actor, + app->search.tab, + app->search.cursor[0] != '\0')) { + indigo_app_search_failed(app, "Could not start the request."); + } break; case INDIGO_REQUEST_LISTS: if (!indigo_session_submit_lists(app->search.cursor[0] != '\0')) { diff --git a/src/ui/layout_bottom.c b/src/ui/layout_bottom.c index e33ae75..10464a7 100644 --- a/src/ui/layout_bottom.c +++ b/src/ui/layout_bottom.c @@ -254,9 +254,16 @@ build_bottom_search(const indigo_app *app, indigo_canvas *c) s->kind == INDIGO_SEARCH_LIKED_BY ? "People who liked this post" : "People who reposted this post"); + } else if (s->kind == INDIGO_SEARCH_AUTHOR) { + /* The box doubles as the tab switch: it names the person and what + * a tap on it will show instead. */ + indigo_canvas_text( + c, q.x + 8, q.y + 8, 0.55f, COL_TEXT_SOFT, "@%.26s Tap: %s", + s->subject, + wf_profile_tab_name(indigo_app_author_tab_after(app))); } else { - indigo_canvas_text(c, q.x + 8, q.y + 8, 0.55f, COL_TEXT_SOFT, "@%.40s", - s->subject); + indigo_canvas_text(c, q.x + 8, q.y + 8, 0.55f, COL_TEXT_SOFT, + "@%.40s", s->subject); } } /* The search screen has no status line in its header to give up, so the diff --git a/src/ui/layout_internal.h b/src/ui/layout_internal.h index 9e4f4c5..5b11d80 100644 --- a/src/ui/layout_internal.h +++ b/src/ui/layout_internal.h @@ -20,6 +20,7 @@ #include "util/buildinfo.h" #include +#include #include #include diff --git a/tests/tests.c b/tests/tests.c index b10d6b4..2eb7918 100644 --- a/tests/tests.c +++ b/tests/tests.c @@ -27,6 +27,7 @@ #include #include #include +#include static int s_checks; static int s_failures; @@ -3377,6 +3378,35 @@ test_author_posts(void) indigo_app_open_author_posts(&app, "someone.else.example"); CHECK(app.search.count == 0); CHECK(strcmp(app.search.subject, "someone.else.example") == 0); + + /* A tap on the header box moves to the next tab and refetches. Someone + * else's tabs skip likes; the signed-in account's do not. */ + indigo_app_take_request(&app, &f); + app.search.loading = false; + CHECK(app.search.tab == WF_PROFILE_TAB_POSTS); + indigo_app_next_author_tab(&app); + CHECK(app.search.tab == WF_PROFILE_TAB_REPLIES); + CHECK(strcmp(indigo_search_title(&app.search), "Replies") == 0); + CHECK(app.search.loading); + CHECK(indigo_app_take_request(&app, &f) == INDIGO_REQUEST_AUTHOR_FEED); + app.search.loading = false; + indigo_app_next_author_tab(&app); + CHECK(app.search.tab == WF_PROFILE_TAB_MEDIA); + app.search.loading = false; + indigo_app_next_author_tab(&app); + CHECK(app.search.tab == WF_PROFILE_TAB_POSTS); + + snprintf(app.signin.account, sizeof app.signin.account, "Me.Example"); + indigo_app_open_author_posts(&app, "me.example"); + app.search.tab = WF_PROFILE_TAB_MEDIA; + app.search.loading = false; + indigo_app_next_author_tab(&app); + CHECK(app.search.tab == WF_PROFILE_TAB_LIKES); + CHECK(strcmp(indigo_search_title(&app.search), "Likes") == 0); + + /* Opening a person afresh starts on their posts again. */ + indigo_app_open_author_posts(&app, "someone.else.example"); + CHECK(app.search.tab == WF_PROFILE_TAB_POSTS); } /* The pinned post costs no request: getProfile already returned the URI, and diff --git a/wolfram.ref b/wolfram.ref index ab4e51c..5203324 100644 --- a/wolfram.ref +++ b/wolfram.ref @@ -1 +1 @@ -v0.35.0 +v0.36.2