Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ the GitHub release notes, so keep the `## [x.y.z] - date` headings exact.

### Fixed

- Going back to a thread fetches its conversation again. The return stack remembers which thread each screen showed, so after a nested unwind the outer thread no longer shows the inner conversation. (#110)
- Backing out of a thread opened from a profile that was itself opened from a thread no longer bounces between the profile and the outer thread. Each screen returns to the one it came from, down to the timeline, through a return stack in place of the single return slots. The snapshot suite now walks that chain. (#110)

## [0.11.1] - 2026-10-16
Expand Down
2 changes: 1 addition & 1 deletion docs/PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ These are Wii U specific and have no row in the other clients.
|---|---|---|
| GamePad touch coverage of the popup and header pills | issue #112 | |
| TV layout uses the extra space | issue #111 | |
| Remember scroll position when returning from a thread or profile | partial #110 | The timeline keeps its cursor and scroll across a thread and across a profile, and notifications across a thread or profile; snapshot checks in `tests/snapshot_main.c` assert it on the host. A nested chain (timeline, thread, profile, thread) used to loop between the profile and the outer thread on B, and now unwinds one screen at a time through a return stack; `tests/snapshot_main.c` asserts it. I could not reproduce a timeline rewind and have not seen it on a console, so issue #110 stays open for the owner to confirm. |
| Remember scroll position when returning from a thread or profile | partial #110 | The timeline keeps its cursor and scroll across a thread and across a profile, and notifications across a thread or profile; snapshot checks in `tests/snapshot_main.c` assert it on the host. A nested chain (timeline, thread, profile, thread) used to loop between the profile and the outer thread on B, and now unwinds one screen at a time through a return stack. Going back to a thread fetches it again, so the outer conversation is the one shown; `tests/snapshot_main.c` asserts both. I could not reproduce a timeline rewind and have not seen it on a console, so issue #110 stays open for the owner to confirm. |
| More menu unified with the thread and profile screens | implemented | Y opens it on the timeline, thread, notifications and profile. The profile's X, Y and + moved into it; notifications get profile, post and refresh. Snapshot checks in `tests/snapshot_main.c`; search results are the timeline screen and already had it. Host only. |
| User guide with controls per screen and screenshots | implemented | `docs/guide.md`, checked by `tools/check-guide.py`. |
| First on-console acceptance pass | issue #24 | Needs the owner's console. |
Expand Down
58 changes: 46 additions & 12 deletions src/app/app.c
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,33 @@ cobalt_app_create(void)
/* A thread or profile opened from `from` pushes it, so B from the new screen
* goes back to it. When the stack is full the oldest return is dropped rather
* than the newest, which is the one B is about to need. */
/* The uri of the post the thread on screen was opened on, which is what
* cobalt_session_begin_thread takes to fetch that conversation again. Empty
* when no thread is loaded. Read it before a new thread is requested, since
* the request replaces the one on screen. */
void
cobalt_app_push_return(cobalt_app *app, cobalt_screen from)
cobalt_app_current_thread_uri(char *out, size_t cap)
{
const cobalt_thread *thread = cobalt_session_thread();

if (!out || cap == 0) {
return;
}
out[0] = '\0';
if (thread->focus >= 0 && thread->focus < thread->count) {
snprintf(out, cap, "%s", thread->posts[thread->focus].uri);
}
}

/* A thread or profile opened from `from` pushes it, so B from the new screen
* goes back to it. `thread_uri` is the conversation `from` showed, when it was
* a thread. When the stack is full the oldest return is dropped rather than the
* newest, which is the one B is about to need. */
void
cobalt_app_push_return(cobalt_app *app, cobalt_screen from, const char *thread_uri)
{
cobalt_return *entry;

if (!app) {
return;
}
Expand All @@ -101,16 +125,26 @@ cobalt_app_push_return(cobalt_app *app, cobalt_screen from)
(COBALT_RETURN_DEPTH - 1) * sizeof(app->return_stack[0]));
app->return_depth--;
}
app->return_stack[app->return_depth++] = from;
entry = &app->return_stack[app->return_depth++];
entry->screen = from;
snprintf(entry->thread_uri, sizeof entry->thread_uri, "%s", thread_uri ? thread_uri : "");
}

/* Returns the screen to go back to. Going back to a thread also fetches that
* conversation again, since the session may hold another one by now. */
cobalt_screen
cobalt_app_pop_return(cobalt_app *app)
{
const cobalt_return *entry;

if (!app || app->return_depth == 0) {
return COBALT_SCREEN_HOME;
}
return app->return_stack[--app->return_depth];
entry = &app->return_stack[--app->return_depth];
if (entry->screen == COBALT_SCREEN_THREAD && entry->thread_uri[0]) {
cobalt_session_begin_thread(entry->thread_uri);
}
return entry->screen;
}

cobalt_screen
Expand Down Expand Up @@ -625,7 +659,7 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_PROFILE_VIEW_OPEN_THREAD:
cobalt_thread_view_reset(&app->thread);
cobalt_app_push_return(app, COBALT_SCREEN_PROFILE);
cobalt_app_push_return(app, COBALT_SCREEN_PROFILE, NULL);
app->screen = COBALT_SCREEN_THREAD;
break;
case COBALT_PROFILE_VIEW_MENU:
Expand All @@ -645,12 +679,12 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_TIMELINE_OPEN_THREAD:
cobalt_thread_view_reset(&app->thread);
cobalt_app_push_return(app, COBALT_SCREEN_TIMELINE);
cobalt_app_push_return(app, COBALT_SCREEN_TIMELINE, NULL);
app->screen = COBALT_SCREEN_THREAD;
break;
case COBALT_TIMELINE_OPEN_PROFILE:
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_TIMELINE);
cobalt_app_push_return(app, COBALT_SCREEN_TIMELINE, NULL);
app->screen = COBALT_SCREEN_PROFILE;
break;
case COBALT_TIMELINE_COMPOSE:
Expand Down Expand Up @@ -718,12 +752,12 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_NOTIFY_OPEN_THREAD:
cobalt_thread_view_reset(&app->thread);
cobalt_app_push_return(app, COBALT_SCREEN_NOTIFICATIONS);
cobalt_app_push_return(app, COBALT_SCREEN_NOTIFICATIONS, NULL);
app->screen = COBALT_SCREEN_THREAD;
break;
case COBALT_NOTIFY_OPEN_PROFILE:
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_NOTIFICATIONS);
cobalt_app_push_return(app, COBALT_SCREEN_NOTIFICATIONS, NULL);
app->screen = COBALT_SCREEN_PROFILE;
break;
case COBALT_NOTIFY_MENU: {
Expand Down Expand Up @@ -878,7 +912,7 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_GRAPH_VIEW_OPEN_PROFILE:
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_FOLLOWS_LIST);
cobalt_app_push_return(app, COBALT_SCREEN_FOLLOWS_LIST, NULL);
app->screen = COBALT_SCREEN_PROFILE;
break;
case COBALT_GRAPH_VIEW_STAY:
Expand All @@ -894,7 +928,7 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_GRAPH_VIEW_OPEN_PROFILE:
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_LIKES_LIST);
cobalt_app_push_return(app, COBALT_SCREEN_LIKES_LIST, NULL);
app->screen = COBALT_SCREEN_PROFILE;
break;
case COBALT_GRAPH_VIEW_STAY:
Expand All @@ -910,7 +944,7 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_SEARCH_VIEW_OPEN_PROFILE:
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_SEARCH);
cobalt_app_push_return(app, COBALT_SCREEN_SEARCH, NULL);
app->screen = COBALT_SCREEN_PROFILE;
break;
case COBALT_SEARCH_VIEW_OPEN_POSTS:
Expand All @@ -931,7 +965,7 @@ app_update_inner(cobalt_app *app, const cobalt_input *in, uint32_t now_ms)
break;
case COBALT_LISTS_VIEW_OPEN_PROFILE:
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_LISTS);
cobalt_app_push_return(app, COBALT_SCREEN_LISTS, NULL);
app->screen = COBALT_SCREEN_PROFILE;
break;
case COBALT_LISTS_VIEW_STAY:
Expand Down
2 changes: 1 addition & 1 deletion src/app/app_home.c
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ cobalt_app_update_account(cobalt_app *app, const cobalt_input *in)
if (self[0]) {
cobalt_session_begin_profile(self);
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, COBALT_SCREEN_ACCOUNT);
cobalt_app_push_return(app, COBALT_SCREEN_ACCOUNT, NULL);
app->screen = COBALT_SCREEN_PROFILE;
COBALT_LOGI("account: opened own profile");
}
Expand Down
13 changes: 11 additions & 2 deletions src/app/app_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,14 @@
* return is dropped. */
#define COBALT_RETURN_DEPTH 16

/* A screen to return to, with the conversation it showed when it was left.
* The session keeps only the last thread it fetched, so returning to a thread
* fetches it again from this uri rather than showing whatever came after. */
typedef struct {
cobalt_screen screen;
char thread_uri[COBALT_POST_URI_MAX];
} cobalt_return;

struct cobalt_app {
cobalt_screen screen;
cobalt_display_mode display;
Expand Down Expand Up @@ -99,7 +107,7 @@ struct cobalt_app {
/* Where B from the thread or profile screen returns to. Each screen that
* opens a thread or profile pushes the one it came from, so a chain such as
* timeline -> thread -> profile -> thread unwinds one step at a time. */
cobalt_screen return_stack[COBALT_RETURN_DEPTH];
cobalt_return return_stack[COBALT_RETURN_DEPTH];
int return_depth;
/* Where B from a likes/reposts list returns to — the timeline or the
* thread. The post it belongs to is already in app->graph.actor. */
Expand Down Expand Up @@ -142,8 +150,9 @@ void cobalt_app_draw_tv_idle(cobalt_render *r);
/* app_menus.c */
void cobalt_app_open_post_menu(cobalt_app *app, const cobalt_post *post, bool in_thread);
void cobalt_app_open_profile_menu(cobalt_app *app);
void cobalt_app_push_return(cobalt_app *app, cobalt_screen from);
void cobalt_app_push_return(cobalt_app *app, cobalt_screen from, const char *thread_uri);
cobalt_screen cobalt_app_pop_return(cobalt_app *app);
void cobalt_app_current_thread_uri(char *out, size_t cap);
void cobalt_app_open_notification_menu(cobalt_app *app, const cobalt_notification *item);
void cobalt_app_popup_choose(cobalt_app *app, int index);

Expand Down
8 changes: 6 additions & 2 deletions src/app/app_menus.c
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,14 @@ void
cobalt_app_popup_choose(cobalt_app *app, int index)
{
const cobalt_popup_item *it = &app->popup.items[index];
char outer[COBALT_POST_URI_MAX];
switch (it->kind) {
case COBALT_POPUP_PROFILE:
case COBALT_POPUP_MENTION:
if (cobalt_session_begin_profile(it->arg)) {
cobalt_profile_view_rewind(&app->profile);
cobalt_app_push_return(app, app->popup_screen);
cobalt_app_current_thread_uri(outer, sizeof outer);
cobalt_app_push_return(app, app->popup_screen, outer);
app->screen = COBALT_SCREEN_PROFILE;
cobalt_popup_close(&app->popup);
}
Expand Down Expand Up @@ -216,9 +218,11 @@ cobalt_app_popup_choose(cobalt_app *app, int index)
}
break;
case COBALT_POPUP_THREAD:
/* Read the conversation on screen before the request replaces it. */
cobalt_app_current_thread_uri(outer, sizeof outer);
if (cobalt_session_begin_thread(it->arg)) {
cobalt_thread_view_reset(&app->thread);
cobalt_app_push_return(app, app->popup_screen);
cobalt_app_push_return(app, app->popup_screen, outer);
app->screen = COBALT_SCREEN_THREAD;
cobalt_popup_close(&app->popup);
}
Expand Down
1 change: 1 addition & 0 deletions src/atproto/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ const cobalt_feed *cobalt_session_feed(void);
* Fetch the conversation around `uri`. Replaces whatever thread was loaded.
*/
bool cobalt_session_begin_thread(const char *uri);
int cobalt_session_thread_requests(void);

/* The thread as last fetched; never NULL. */
const cobalt_thread *cobalt_session_thread(void);
Expand Down
16 changes: 15 additions & 1 deletion src/atproto/session_read.c
Original file line number Diff line number Diff line change
Expand Up @@ -794,6 +794,10 @@ cobalt_session_chat_started_convo(void)
return g_session.chat_started;
}

/* How many thread requests were accepted, so a test can see a conversation was
* fetched again without depending on when the network answers. */
static int g_thread_requests;

bool
cobalt_session_begin_thread(const char *uri)
{
Expand All @@ -804,5 +808,15 @@ cobalt_session_begin_thread(const char *uri)
cobalt_job_input in;
memset(&in, 0, sizeof(in));
snprintf(in.uri, sizeof(in.uri), "%s", uri);
return cobalt_session_submit(COBALT_JOB_THREAD, &in);
if (!cobalt_session_submit(COBALT_JOB_THREAD, &in)) {
return false;
}
g_thread_requests++;
return true;
}

int
cobalt_session_thread_requests(void)
{
return g_thread_requests;
}
7 changes: 6 additions & 1 deletion tests/snapshot_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -375,7 +375,12 @@ main(int argc, char **argv)
CHECK(cobalt_app_screen(g_app) == COBALT_SCREEN_THREAD);
frame(COBALT_BTN_BACK); settle(10);
CHECK(cobalt_app_screen(g_app) == COBALT_SCREEN_PROFILE);
frame(COBALT_BTN_BACK); settle(10);
/* Going back to the outer thread fetches it again: the session held the
* inner conversation, so without the refetch the outer screen showed it. */
const int requests = cobalt_session_thread_requests();
frame(COBALT_BTN_BACK);
CHECK(cobalt_session_thread_requests() == requests + 1);
settle(10);
CHECK(cobalt_app_screen(g_app) == COBALT_SCREEN_THREAD);
frame(COBALT_BTN_BACK); settle(10);
CHECK(cobalt_app_screen(g_app) == COBALT_SCREEN_TIMELINE);
Expand Down
Loading