From a4a1990dbb16e3e4599500299c32c980c9cbd5bf Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 25 Oct 2025 16:21:42 +0200 Subject: rendering visibility conditionally, with lesser reach if needed --- html.c | 72 +++++++++++++++++++++++++++++------------------------------------- 1 file changed, 32 insertions(+), 40 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 584059b..7c39237 100644 --- a/html.c +++ b/html.c @@ -398,6 +398,37 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, return actor_icon; } +void html_note_render_visibility(snac* user, xs_html *form, const int scope) +{ + // scopes aren't sorted by value unfortunately, so simple math won't work to limit them. using map-like thing here + static const int public_scopes[5] = {SCOPE_PUBLIC, SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1}; + static const int unlisted_scopes[4] = {SCOPE_UNLISTED, SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1}; + static const int followers_scopes[3] = {SCOPE_FOLLOWERS, SCOPE_MENTIONED, -1}; + static const int mentioned_scopes[2] = {SCOPE_MENTIONED, -1}; + static const int * const scopes[4] = { public_scopes, mentioned_scopes, unlisted_scopes, followers_scopes}; + static const char * const scopes_tags[4] = { "public", "mentioned", "unlisted", "followers"}; + static const char * const scopes_names[4] = { "Public", "Direct Message", "Unlisted", "Followers-only"}; + + xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: "))); + const int* to_render = scopes[scope]; + for( int i = 0; to_render[i] != -1; i++ ){ + const int scope_i = to_render[i]; + const char* value = scopes_tags[scope_i]; + const char* name = scopes_names[scope_i]; + xs_html_add(paragraph, + xs_html_tag("label", + xs_html_sctag("input", + xs_html_attr("type", "radio"), + xs_html_attr("name", "visibility"), + xs_html_attr("value", value), + xs_html_attr(scope == scope_i ? "checked" : "", NULL)), + xs_html_text(" "), + xs_html_text(L(name)), + xs_html_text(" ")) + ); + } + xs_html_add(form, paragraph); +} xs_html *html_note(snac *user, const char *summary, const char *div_id, const char *form_id, @@ -455,46 +486,7 @@ xs_html *html_note(snac *user, const char *summary, xs_html_attr("type", "hidden"), xs_html_attr("name", "to"), xs_html_attr("value", actor_id))); - else { - xs_html_add(form, - xs_html_tag("p", - xs_html_text(L("Visibility: ")), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "public"), - xs_html_attr(scope == SCOPE_PUBLIC ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Public"))), - xs_html_text(" "), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "unlisted"), - xs_html_attr(scope == SCOPE_UNLISTED ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Unlisted"))), - xs_html_text(" "), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "followers"), - xs_html_attr(scope == SCOPE_FOLLOWERS ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Followers-only"))), - xs_html_text(" "), - xs_html_tag("label", - xs_html_sctag("input", - xs_html_attr("type", "radio"), - xs_html_attr("name", "visibility"), - xs_html_attr("value", "mentioned"), - xs_html_attr(scope == SCOPE_MENTIONED ? "checked" : "", NULL)), - xs_html_text(" "), - xs_html_text(L("Direct Message"))))); - } + html_note_render_visibility(user, form, scope); if (redir) xs_html_add(form, -- cgit v1.2.3 From daee7581dbf5b458f28e486fd89804b91209a9bb Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 1 Nov 2025 01:18:28 +0100 Subject: no visibility for editing --- html.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a2dfb79..84636e9 100644 --- a/html.c +++ b/html.c @@ -486,7 +486,8 @@ xs_html *html_note(snac *user, const char *summary, xs_html_attr("type", "hidden"), xs_html_attr("name", "to"), xs_html_attr("value", actor_id))); - html_note_render_visibility(user, form, scope); + if (edit_id == NULL) + html_note_render_visibility(user, form, scope); if (redir) xs_html_add(form, -- cgit v1.2.3 From fcae55fcb1542f6fa0a330c45e3b806f902a84de Mon Sep 17 00:00:00 2001 From: grunfink Date: Wed, 26 Nov 2025 17:21:16 +0100 Subject: Some attachments are of type 'Video' instead of a MIME type (bsky). --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index df5b508..12d269a 100644 --- a/html.c +++ b/html.c @@ -2634,7 +2634,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_attr("title", name)))); } else - if (xs_startswith(type, "video/")) { + if (xs_startswith(type, "video/") || strcmp(type, "Video") == 0) { xs_html_add(content_attachments, xs_html_tag("video", xs_html_attr("preload", "none"), -- cgit v1.2.3 From a45c1ce152011e8fe25eb1d25594ac5705f65404 Mon Sep 17 00:00:00 2001 From: rako Date: Fri, 28 Nov 2025 10:37:49 +0100 Subject: Fix user matching In order to be a proper prefix, the actor url must end with a '/' otherwise it can match another user that starts with the same prefix: for example 'testuser' will match anything made by 'testuser2' --- html.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 12d269a..d6223b9 100644 --- a/html.c +++ b/html.c @@ -1898,7 +1898,7 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_attr("name", "redir"), xs_html_attr("value", redir)))); - if (!xs_startswith(id, user->actor)) { + if (!is_msg_mine(user, id)) { if (xs_list_in(likes, user->md5) == -1) { /* not already liked; add button */ xs_html_add(form, @@ -2426,7 +2426,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (read_only) closed = 1; /* non-identified page; show as closed */ else - if (user && xs_startswith(id, user->actor)) + if (user && is_msg_mine(user, id)) closed = 1; /* we questioned; closed for us */ else if (user && was_question_voted(user, id)) @@ -5022,7 +5022,7 @@ int html_post_handler(const xs_dict *req, const char *q_path, } else { /* delete an entry */ - if (xs_startswith(id, snac.actor) && !is_draft(&snac, id)) { + if (is_msg_mine(&snac, id) && !is_draft(&snac, id)) { /* it's a post by us: generate a delete */ xs *msg = msg_delete(&snac, id); -- cgit v1.2.3 From ee7483cf0dd007d104a815fce47a0d5f050ca870 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 9 Dec 2025 13:31:37 +0100 Subject: New query variable terse=anything. If set to any value, a public post page doesn't show any headline. --- html.c | 38 +++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 9ac57f8..32ed057 100644 --- a/html.c +++ b/html.c @@ -2996,7 +2996,7 @@ xs_html *html_footer(const snac *user) xs_str *html_timeline(snac *user, const xs_list *list, int read_only, int skip, int show, int show_more, const char *title, const char *page, - int utl, const char *error) + int utl, const char *error, int terse) /* returns the HTML for the timeline */ { xs_list *p = (xs_list *)list; @@ -3024,7 +3024,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, if (user) { head = html_user_head(user, desc, alternate); - body = html_user_body(user, read_only); + + if (terse) + body = xs_html_tag("body", NULL); + else + body = html_user_body(user, read_only); } else { head = html_instance_head(); @@ -3902,6 +3906,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, int cache = 1; int save = 1; int proxy = 0; + int terse = 0; const char *v; const xs_dict *q_vars = xs_dict_get(req, "q_vars"); @@ -3914,6 +3919,9 @@ int html_get_handler(const xs_dict *req, const char *q_path, return HTTP_STATUS_NOT_FOUND; } + if (!xs_is_null(xs_dict_get(q_vars, "terse"))) + terse = 1; + if (strcmp(v, "share-bridge") == 0) { /* temporary redirect for a post */ const char *login = xs_dict_get(q_vars, "login"); @@ -4014,7 +4022,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, if (xs_type(xs_dict_get(snac.config, "private")) == XSTYPE_TRUE) { /** empty public timeline for private users **/ - *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error); + *body = html_timeline(&snac, NULL, 1, 0, 0, 0, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4037,7 +4045,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *pins = pinned_list(&snac); pins = xs_list_cat(pins, list); - *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error); + *body = html_timeline(&snac, pins, 1, skip, show, more, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -4161,7 +4169,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(xs_list_len(tl) ? L("Search results for tag %s") : L("Nothing found for tag %s"), q); - *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error); + *body = html_timeline(&snac, tl, 0, skip, show, more, title, page, 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4186,7 +4194,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, title = xs_fmt(L("Nothing found for '%s'"), q); *body = html_timeline(&snac, tl, 0, skip, tl_len, to || tl_len == show, - title, page, 0, error); + title, page, 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4213,7 +4221,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = timeline_list(&snac, "private", skip, show, &more); *body = html_timeline(&snac, list, 0, skip, show, - more, NULL, "/admin", 1, error); + more, NULL, "/admin", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; @@ -4240,7 +4248,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list0 = xs_list_append(xs_list_new(), md5); xs *list = timeline_top_level(&snac, list0); - *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error); + *body = html_timeline(&snac, list, 0, 0, 0, 0, NULL, "/admin", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4281,7 +4289,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *next = timeline_instance_list(skip + show, 1); *body = html_timeline(&snac, list, 0, skip, show, - xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error); + xs_list_len(next), L("Showing instance timeline"), "/instance", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4296,7 +4304,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = pinned_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Pinned posts"), "", 0, error); + 0, L("Pinned posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4311,7 +4319,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = bookmark_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Bookmarked posts"), "", 0, error); + 0, L("Bookmarked posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4326,7 +4334,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = draft_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Post drafts"), "", 0, error); + 0, L("Post drafts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4341,7 +4349,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *list = scheduled_list(&snac); *body = html_timeline(&snac, list, 0, skip, show, - 0, L("Scheduled posts"), "", 0, error); + 0, L("Scheduled posts"), "", 0, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4367,7 +4375,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(L("Showing timeline for list '%s'"), name); *body = html_timeline(&snac, ttl, 0, skip, show, - xs_list_len(next), title, base, 1, error); + xs_list_len(next), title, base, 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } @@ -4387,7 +4395,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, list = xs_list_append(list, md5); - *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error); + *body = html_timeline(&snac, list, 1, 0, 0, 0, NULL, "", 1, error, terse); *b_size = strlen(*body); status = HTTP_STATUS_OK; } -- cgit v1.2.3 From 481c0315ef7ccd2eaf0ec9c6eaa17cbbd7efeb2c Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 9 Dec 2025 13:38:56 +0100 Subject: If terse is set, history is also hidden. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 32ed057..8f7c4a9 100644 --- a/html.c +++ b/html.c @@ -3248,7 +3248,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, if (list && user && read_only) { /** history **/ - if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE) { + if (xs_type(xs_dict_get(srv_config, "disable_history")) != XSTYPE_TRUE && !terse) { xs_html *ul = xs_html_tag("ul", NULL); xs_html *history = xs_html_tag("div", -- cgit v1.2.3 From 85ed0eb0d535700a5df837c37f51848811e461a0 Mon Sep 17 00:00:00 2001 From: violette Date: Thu, 18 Dec 2025 07:58:24 +0100 Subject: Added emoji reactions (contributed by violette). --- html.c | 317 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 305 insertions(+), 12 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 8f7c4a9..8cc0067 100644 --- a/html.c +++ b/html.c @@ -54,9 +54,10 @@ int login(snac *user, const xs_dict *headers) return logged_in; } - -xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy) -/* replaces all the :shortnames: with the emojis in tag */ +xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, + const char *proxy, const xs_list *cl, const char *act) +/* replace but also adds a class list and an actor in its alt text. + * Used for emoji reactions */ { if (!xs_is_null(tag)) { xs *tag_list = NULL; @@ -69,11 +70,15 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p tag_list = xs_dup(tag); } - xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems); + xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems); xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems); + if (cl) + class = xs_str_cat(class, " ", xs_join(cl, " ")); - const xs_dict *v; int c = 0; + const xs_val *v; + + c = 0; xs_set rep_emoji; xs_set_init(&rep_emoji); @@ -100,6 +105,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); + act = act ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); + if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); else { @@ -108,8 +115,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p xs_html *img = xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("src", url), - xs_html_attr("alt", n), - xs_html_attr("title", n), + xs_html_attr("alt", act), + xs_html_attr("title", act), xs_html_attr("class", class), xs_html_attr("style", style)); @@ -130,6 +137,13 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p } +xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *proxy) +/* replaces all the :shortnames: with the emojis in tag */ +{ + return _replace_shortnames(s, tag, ems, proxy, NULL, NULL); +} + + xs_str *actor_name(xs_dict *actor, const char *proxy) /* gets the actor name */ { @@ -430,6 +444,52 @@ void html_note_render_visibility(snac* user, xs_html *form, const int scope) xs_html_add(form, paragraph); } +/* html_note but moddled for emoji's needs. here and not bellow, since the + * other one is already so complex. */ +xs_html *html_emoji(snac *user, const char *summary, + const char *div_id, const char *form_id, + const char* placeholder, const char *post_id, + const char* eid) +{ + xs *action = xs_fmt("%s/admin/action", user->actor); + + xs_html *form; + const int react = eid == NULL ? 0 : 1; + + xs_html *note = xs_html_tag("div", + xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(summary)), + xs_html_tag("p", NULL), + xs_html_tag("div", + xs_html_attr("class", "snac-note"), + xs_html_attr("id", div_id), + form = xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", action), + xs_html_attr("enctype", "multipart/form-data"), + xs_html_attr("id", form_id), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "id"), + xs_html_attr("value", post_id)), + xs_html_sctag("input", + xs_html_attr("type", react ? "hidden" : "text"), + xs_html_attr("name", "eid"), + xs_html_attr(react ? "value" : "placeholder", react ? eid : placeholder)), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("eid", "action"), + xs_html_attr("value", react ? L("EmojiUnreact") : L("EmojiReact"))), + xs_html_text(" "), + xs_html_tag("p", NULL))))); + + return note; +} + xs_html *html_note(snac *user, const char *summary, const char *div_id, const char *form_id, const char *ta_plh, const char *ta_content, @@ -1356,6 +1416,28 @@ xs_html *html_top_controls(snac *user) xs_html_attr("value", L("Like"))), xs_html_text(" "), xs_html_text(L("(by URL)"))), + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", ops_action), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "eid"), + xs_html_attr("required", "required"), + xs_html_attr("placeholder", ":neocat:")), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "text"), + xs_html_attr("name", "id"), + xs_html_attr("required", "required"), + xs_html_attr("placeholder", "https:/" "/fedi.example.com/bob/...")), + xs_html_text(" "), + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("value", L("EmojiReact"))), + xs_html_text(" "), + xs_html_text(L("(by URL)"))), xs_html_tag("p", NULL))); /** user settings **/ @@ -2019,6 +2101,21 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_tag("p", NULL)); } + { /** emoji react **/ + /* the post textarea */ + xs *div_id = xs_fmt("%s_reply", md5); + xs *form_id = xs_fmt("%s_reply_form", md5); + + xs_html_add(controls, xs_html_tag("div", + xs_html_tag("p", NULL), + html_emoji( + user, L("Emoji react"), + div_id, form_id, + ":neocat:", id, + emoji_reacted(user, id))), + xs_html_tag("p", NULL)); + } + { /** reply **/ /* the post textarea */ xs *ct = build_mentions(user, msg); @@ -2345,6 +2442,168 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_add(snac_content_wrap, snac_content); + /* add all emoji reacts */ + int is_emoji = 0; + { + int c = 0; + const xs_dict *k; + xs *ls = xs_list_new(); + xs *sfrl = xs_dict_new(); + xs *rl = object_get_emoji_reacts(id); + + xs_dict *m = NULL; + while (xs_list_next(rl, &v, &c)) { + if (valid_status(object_get_by_md5(v, &m))) { + const char *content = xs_dict_get(m, "content"); + const char *actor = xs_dict_get(m, "actor"); + const xs_list *contentl = xs_dict_get(sfrl, content); + xs *actors = xs_list_new(); + actors = xs_list_append(actors, actor); + char me = actor && user && strcmp(actor, user->actor) == 0; + int count = 1; + + if (contentl) { + count = atoi(xs_list_get(contentl, 0)) + 1; + const xs_list *actorsc = xs_list_get(contentl, 1); + if (strncmp(xs_list_get(contentl, 2), "1", 1) == 0) + me = 1; + + if (xs_list_in(actorsc, actor) != -1) { + xs_free(actors); + actors = xs_dup(actorsc); + } + else + actors = xs_list_cat(actors, actorsc); + } + + xs *fl = xs_list_new(); + fl = xs_list_append(fl, xs_fmt("%d", count), actors, xs_fmt("%d", me)); + sfrl = xs_dict_append(sfrl, content, fl); + } + } + + c = 0; + + while (xs_list_next(rl, &k, &c)) { + if (valid_status(object_get_by_md5(k, &m))) { + const xs_dict *tag = xs_dict_get(m, "tag"); + const xs_dict *ide = xs_dict_get(m, "id"); + + const char *content = xs_dict_get(m, "content"); + const char *shortname; + shortname = xs_dict_get(m, "content"); + + const xs_list *items = xs_dict_get(sfrl, content); + const char *nb = xs_list_get(items, 0); + const xs_list *actors = xs_list_get(items, 1); + const char me = *xs_list_get(items, 2) == '1'; + + if (!xs_is_null(nb)) { + is_emoji = 1; + + const char *act = atoi(nb) > 1 ? + xs_fmt("%d different actors \n\t%s", atoi(nb), xs_join(actors, ",\n\t")) : + xs_dict_get(m, "actor"); + + xs *class = xs_list_new(); + class = xs_list_append(class, "snac-reaction"); + + xs_html *ret = NULL; + if (tag && shortname) { + xs *cl = xs_list_new(); + cl = xs_list_append(cl, "snac-reaction-image"); + xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act); + + if (me) + class = xs_list_append(class, "snac-reacted"); + + ret = xs_html_tag("button", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("value", me ? L("EmojiReact") : L("EmojiUnreact")), + xs_html_raw(emoji), + xs_html_tag("span", + xs_html_raw(nb), + xs_html_attr("style", "padding-left: 5px;")), + xs_html_attr("title", act), + xs_html_attr("class", xs_join(class, " "))); + + if (!(ide && xs_startswith(ide, srv_baseurl))) + xs_html_add(ret, xs_html_attr("disabled", "true")); + } + else if (shortname) { + xs *sn = xs_dup(shortname); + const char *sna = sn; + unsigned int utf = xs_utf8_dec((const char **)&sna); + + if (xs_is_emoji(utf)) { + const char *style = "font-size: large;"; + if (me) + class = xs_list_append(class, "snac-reacted"); + ret = xs_html_tag("button", + xs_html_attr("type", "submit"), + xs_html_attr("name", "action"), + xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")), + xs_html_raw(xs_fmt("&#%d", utf)), + xs_html_tag("span", + xs_html_raw(nb), + xs_html_attr("style", "font-size: initial; padding-left: 5px;")), + xs_html_attr("title", act), + xs_html_attr("class", xs_join(class, " ")), + xs_html_attr("style", style)); + } + } + if (ret) { + xs *s1; + if (user) { + xs *action = xs_fmt("%s/admin/action", user->actor); + xs *form_id = xs_fmt("%s_reply_form", md5); + + xs_html *form = + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", action), + xs_html_attr("enctype", "multipart/form-data"), + xs_html_attr("style", "display: inline-flex;" + "vertical-align: middle;"), + xs_html_attr("id", form_id), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "id"), + xs_html_attr("value", id)), + xs_html_sctag("input", + xs_html_attr("type", "hidden"), + xs_html_attr("name", "eid"), + xs_html_attr("value", shortname)), + ret); + s1 = xs_html_render(form); + } + else + s1 = xs_html_render(ret); + + ls = xs_list_append(ls, s1); + sfrl = xs_dict_del(sfrl, content); + } + } + } + } + + c = 0; + + xs_html *emoji_div; + if (xs_list_len(ls) > 0) { + emoji_div = xs_html_tag("div", xs_html_text(L("Emoji reactions: ")), + xs_html_attr("class", "snac-reaction-div")); + + while (ls != NULL && xs_list_next(ls, &k, &c)) + xs_html_add(emoji_div, xs_html_raw(k)); + + xs_html_add(snac_content_wrap, emoji_div); + } + + } + { /** build the content string **/ const char *content = xs_dict_get(msg, "content"); @@ -2371,7 +2630,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, c = xs_replace_i(c, "

", "

"); - c = xs_str_cat(c, "

"); + if (is_emoji == 0) + c = xs_str_cat(c, "

"); /* replace the :shortnames: */ c = replace_shortnames(c, xs_dict_get(msg, "tag"), 2, proxy); @@ -3686,9 +3946,18 @@ xs_str *html_notifications(snac *user, int skip, int show) if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) { const char *content = xs_dict_get_path(noti, "msg.content"); + xs *cd = xs_dup(content); + const char *sna = cd; + const xs_dict *tag = xs_dict_get_path(noti, "msg.tag"); + unsigned int utf = xs_utf8_dec((const char **)&sna); + + int isEmoji = 0; + if (xs_is_emoji(utf) || (tag && xs_list_len(tag) > 0)) + isEmoji = 1; + if (xs_type(content) == XSTYPE_STRING) { xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy); - wrk = xs_fmt("%s (%s️)", type, emoji); + wrk = xs_fmt("%s (%s️)", isEmoji ? "EmojiReact" : "Like", emoji); label = wrk; } } @@ -4583,8 +4852,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *msg = msg_admiration(&snac, id, *action == 'L' ? "Like" : "Announce"); if (msg != NULL) { + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0, msg); enqueue_message(&snac, msg); - timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, *action == 'L' ? 1 : 0); status = HTTP_STATUS_SEE_OTHER; } @@ -4892,12 +5161,36 @@ int html_post_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_SEE_OTHER; + if (strcmp(action, L("EmojiUnreact")) == 0) { /** **/ + const char *eid = xs_dict_get(p_vars, "eid"); + + if (eid != NULL) { + xs *n_msg = msg_emoji_unreact(&snac, id, eid); + + if (n_msg != NULL) + enqueue_message(&snac, n_msg); + } + } + else + if (strcmp(action, L("EmojiReact")) == 0) { /** **/ + const char *eid = xs_dict_get(p_vars, "eid"); + + eid = xs_strip_chars_i(xs_dup(eid), ":"); + + const xs_dict *ret = msg_emoji_init(&snac, id, eid); + /* fails if either invalid or already reacted */ + if (!ret) + ret = msg_emoji_unreact(&snac, id, eid); + if (!ret) + status = HTTP_STATUS_NOT_FOUND; + } + else if (strcmp(action, L("Like")) == 0) { /** **/ xs *msg = msg_admiration(&snac, id, "Like"); if (msg != NULL) { + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1, msg); enqueue_message(&snac, msg); - timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 1); } } else @@ -4905,8 +5198,8 @@ int html_post_handler(const xs_dict *req, const char *q_path, xs *msg = msg_admiration(&snac, id, "Announce"); if (msg != NULL) { + timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0, msg); enqueue_message(&snac, msg); - timeline_admire(&snac, xs_dict_get(msg, "object"), snac.actor, 0); } } else -- cgit v1.2.3 From 7c065cbc9879582985cef3d3ad8b2ae197b8b851 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:09:10 +0100 Subject: Fixed some xs_dup() leaks. --- html.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 8cc0067..dae84b7 100644 --- a/html.c +++ b/html.c @@ -5173,9 +5173,9 @@ int html_post_handler(const xs_dict *req, const char *q_path, } else if (strcmp(action, L("EmojiReact")) == 0) { /** **/ - const char *eid = xs_dict_get(p_vars, "eid"); + xs *eid = xs_dup(xs_dict_get(p_vars, "eid")); - eid = xs_strip_chars_i(xs_dup(eid), ":"); + eid = xs_strip_chars_i(eid, ":"); const xs_dict *ret = msg_emoji_init(&snac, id, eid); /* fails if either invalid or already reacted */ -- cgit v1.2.3 From ce667add2cfedcc6d57c521899dfb9d2e3e6e1d7 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:13:54 +0100 Subject: Fixed some xs_join() uses. --- html.c | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index dae84b7..6ee0707 100644 --- a/html.c +++ b/html.c @@ -72,8 +72,10 @@ xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, xs *style = xs_fmt("max-height: %dem; max-width: %dem;", ems, ems); xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems); - if (cl) - class = xs_str_cat(class, " ", xs_join(cl, " ")); + if (cl) { + xs *l = xs_join(cl, " "); + class = xs_str_cat(class, " ", l); + } int c = 0; const xs_val *v; @@ -2501,9 +2503,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (!xs_is_null(nb)) { is_emoji = 1; - const char *act = atoi(nb) > 1 ? - xs_fmt("%d different actors \n\t%s", atoi(nb), xs_join(actors, ",\n\t")) : - xs_dict_get(m, "actor"); + xs *al = xs_join(actors, ",\n\t"); + xs *act = atoi(nb) > 1 ? + xs_fmt("%d different actors \n\t%s", atoi(nb), al) : + xs_dup(xs_dict_get(m, "actor")); xs *class = xs_list_new(); class = xs_list_append(class, "snac-reaction"); @@ -2517,6 +2520,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (me) class = xs_list_append(class, "snac-reacted"); + xs *l1 = xs_join(class, " "); ret = xs_html_tag("button", xs_html_attr("type", "submit"), xs_html_attr("name", "action"), @@ -2526,7 +2530,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_raw(nb), xs_html_attr("style", "padding-left: 5px;")), xs_html_attr("title", act), - xs_html_attr("class", xs_join(class, " "))); + xs_html_attr("class", l1)); if (!(ide && xs_startswith(ide, srv_baseurl))) xs_html_add(ret, xs_html_attr("disabled", "true")); @@ -2540,6 +2544,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *style = "font-size: large;"; if (me) class = xs_list_append(class, "snac-reacted"); + xs *l1 = xs_join(class, " "); ret = xs_html_tag("button", xs_html_attr("type", "submit"), xs_html_attr("name", "action"), @@ -2549,7 +2554,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_raw(nb), xs_html_attr("style", "font-size: initial; padding-left: 5px;")), xs_html_attr("title", act), - xs_html_attr("class", xs_join(class, " ")), + xs_html_attr("class", l1), xs_html_attr("style", style)); } } -- cgit v1.2.3 From c8ee70f6df7276b7c02f902cc544d209139697b2 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:20:15 +0100 Subject: Fixed some xs_fmt() usages. --- html.c | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 6ee0707..c317c1a 100644 --- a/html.c +++ b/html.c @@ -55,7 +55,7 @@ int login(snac *user, const xs_dict *headers) } xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, - const char *proxy, const xs_list *cl, const char *act) + const char *proxy, const xs_list *cl, const char *act_o) /* replace but also adds a class list and an actor in its alt text. * Used for emoji reactions */ { @@ -107,7 +107,7 @@ xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); - act = act ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); + xs *act = act_o ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); @@ -2479,7 +2479,9 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } xs *fl = xs_list_new(); - fl = xs_list_append(fl, xs_fmt("%d", count), actors, xs_fmt("%d", me)); + xs *c1 = xs_fmt("%d", count); + xs *c2 = xs_fmt("%d", me); + fl = xs_list_append(fl, c1, actors, c2); sfrl = xs_dict_append(sfrl, content, fl); } } @@ -2545,11 +2547,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, if (me) class = xs_list_append(class, "snac-reacted"); xs *l1 = xs_join(class, " "); + xs *s1 = xs_fmt("&#%d", utf); ret = xs_html_tag("button", xs_html_attr("type", "submit"), xs_html_attr("name", "action"), xs_html_attr("value", me ? L("EmojiUnreact") : L("EmojiReact")), - xs_html_raw(xs_fmt("&#%d", utf)), + xs_html_raw(s1), xs_html_tag("span", xs_html_raw(nb), xs_html_attr("style", "font-size: initial; padding-left: 5px;")), -- cgit v1.2.3 From 219f3e6808bdcf8a03cce404caa5cb6a008d00c5 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:54:21 +0100 Subject: Fixed leak in emoji_reacted(). --- html.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index c317c1a..48273f2 100644 --- a/html.c +++ b/html.c @@ -2107,14 +2107,15 @@ xs_html *html_entry_controls(snac *user, const char *actor, /* the post textarea */ xs *div_id = xs_fmt("%s_reply", md5); xs *form_id = xs_fmt("%s_reply_form", md5); + xs *e_react = emoji_reacted(user, id); xs_html_add(controls, xs_html_tag("div", xs_html_tag("p", NULL), html_emoji( - user, L("Emoji react"), + user, L("Emoji react..."), div_id, form_id, ":neocat:", id, - emoji_reacted(user, id))), + e_react)), xs_html_tag("p", NULL)); } -- cgit v1.2.3 From 2903b29e653d8549319ceabbb65de408d2e5c073 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 08:58:04 +0100 Subject: Fixed more minor leaks. --- html.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 48273f2..8700ac4 100644 --- a/html.c +++ b/html.c @@ -2454,8 +2454,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs *sfrl = xs_dict_new(); xs *rl = object_get_emoji_reacts(id); - xs_dict *m = NULL; while (xs_list_next(rl, &v, &c)) { + xs *m = NULL; if (valid_status(object_get_by_md5(v, &m))) { const char *content = xs_dict_get(m, "content"); const char *actor = xs_dict_get(m, "actor"); @@ -2490,6 +2490,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, c = 0; while (xs_list_next(rl, &k, &c)) { + xs *m = NULL; if (valid_status(object_get_by_md5(k, &m))) { const xs_dict *tag = xs_dict_get(m, "tag"); const xs_dict *ide = xs_dict_get(m, "id"); -- cgit v1.2.3 From da6d275933e9dac513386ac2bc567d3568ab0217 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 09:45:55 +0100 Subject: Strip surrounding colons in emoji pictures, if they are still there. --- html.c | 2 ++ 1 file changed, 2 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index 8700ac4..e59e622 100644 --- a/html.c +++ b/html.c @@ -2521,6 +2521,8 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, cl = xs_list_append(cl, "snac-reaction-image"); xs *emoji = _replace_shortnames(xs_dup(shortname), tag, 2, proxy, cl, act); + emoji = xs_strip_chars_i(emoji, ":"); + if (me) class = xs_list_append(class, "snac-reacted"); -- cgit v1.2.3 From 9f3f8d237ac32f60c8471515dcb64095f1c4ccd6 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 18 Dec 2025 10:02:43 +0100 Subject: Fixed typo. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index e59e622..f1b004b 100644 --- a/html.c +++ b/html.c @@ -107,7 +107,7 @@ xs_str *_replace_shortnames(xs_str *s, const xs_list *tag, int ems, if (!xs_is_string(mt)) mt = xs_mime_by_ext(u); - xs *act = act_o ? xs_fmt("%s\n%s", n, act) : xs_fmt("%s", n); + xs *act = act_o ? xs_fmt("%s\n%s", n, act_o) : xs_fmt("%s", n); if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) s = xs_replace_i(s, n, ""); -- cgit v1.2.3 From e233f3f904e64d6ecaffdf68f8532e1587681fe3 Mon Sep 17 00:00:00 2001 From: violette Date: Fri, 19 Dec 2025 09:54:50 +0100 Subject: Some fixes. --- html.c | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index f1b004b..f6c93c7 100644 --- a/html.c +++ b/html.c @@ -2500,11 +2500,12 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, shortname = xs_dict_get(m, "content"); const xs_list *items = xs_dict_get(sfrl, content); - const char *nb = xs_list_get(items, 0); - const xs_list *actors = xs_list_get(items, 1); - const char me = *xs_list_get(items, 2) == '1'; - if (!xs_is_null(nb)) { + if (!xs_is_null(items)) { + const char *nb = xs_list_get(items, 0); + const xs_list *actors = xs_list_get(items, 1); + const char me = *xs_list_get(items, 2) == '1'; + is_emoji = 1; xs *al = xs_join(actors, ",\n\t"); -- cgit v1.2.3 From a97d3ac11628cd9244f97d2304d972abfb2b4824 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 19 Dec 2025 15:27:51 +0100 Subject: Reindex tags when editing a post. Tags that were deleted in the post message are not deleted, though. --- html.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index f6c93c7..0b84e56 100644 --- a/html.c +++ b/html.c @@ -5141,6 +5141,9 @@ int html_post_handler(const xs_dict *req, const char *q_path, /* overwrite object, not updating the indexes */ object_add_ow(edit_id, msg); + /* index tags */ + tag_index(edit_id, msg); + /* update message */ c_msg = msg_update(&snac, msg); } -- cgit v1.2.3 From 79c32485dffa95107216c2251ffa111d7cdef432 Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 20 Dec 2025 13:15:34 +0100 Subject: Fixed failed search when blank surround the query string. --- html.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 0b84e56..20ad839 100644 --- a/html.c +++ b/html.c @@ -4343,9 +4343,13 @@ int html_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_UNAUTHORIZED; } else { - const char *q = xs_dict_get(q_vars, "q"); + xs *q = NULL; + const char *q1 = xs_dict_get(q_vars, "q"); xs *url_acct = NULL; + if (xs_is_string(q1)) + q = xs_strip_i(xs_dup(q1)); + /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ -- cgit v1.2.3 From a1f0a89931ad1ac51e28c318e29ef7f1cfc25255 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 26 Dec 2025 21:15:19 +0100 Subject: Revert "Fixed failed search when blank surround the query string." This reverts commit 79c32485dffa95107216c2251ffa111d7cdef432. --- html.c | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 20ad839..0b84e56 100644 --- a/html.c +++ b/html.c @@ -4343,13 +4343,9 @@ int html_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_UNAUTHORIZED; } else { - xs *q = NULL; - const char *q1 = xs_dict_get(q_vars, "q"); + const char *q = xs_dict_get(q_vars, "q"); xs *url_acct = NULL; - if (xs_is_string(q1)) - q = xs_strip_i(xs_dup(q1)); - /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ -- cgit v1.2.3 From be4dd4ea7f58a31546ad873d5b464fdb8b289ec8 Mon Sep 17 00:00:00 2001 From: grunfink Date: Fri, 26 Dec 2025 21:24:08 +0100 Subject: Fixed failed search when blank surround the query string. This time, without fucking up everything. --- html.c | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 0b84e56..3e692bd 100644 --- a/html.c +++ b/html.c @@ -4343,9 +4343,15 @@ int html_get_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_UNAUTHORIZED; } else { - const char *q = xs_dict_get(q_vars, "q"); + const char *q = NULL; + xs *cq = xs_dup(xs_dict_get(q_vars, "q")); xs *url_acct = NULL; + if (xs_is_string(cq)) { + cq = xs_strip_i(cq); + q = cq; + } + /* searching for an URL? */ if (q && xs_match(q, "https://*|http://*")) { /* may by an actor; try a webfinger */ -- cgit v1.2.3 From b64bebd412f10e7a29f92220624e8423aaf5d883 Mon Sep 17 00:00:00 2001 From: Alexandre Oliva Date: Sun, 28 Dec 2025 20:59:12 +0100 Subject: introduce separate people/ pages When you have lots of followers or followees or pending follows, constructing the entire people page to look up information about a single user can take a while and be quite wasteful when you want to look up a single user. Introduce and prefer people/ over people#. While at that, fix a memory leak in webfinger search: the empty list was allocated twice. --- html.c | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 3 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 3e692bd..c03c867 100644 --- a/html.c +++ b/html.c @@ -221,7 +221,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, anchored link to the people page instead of the actor url */ if (fwer || fwing) { xs *md5 = xs_md5_hex(actor_id, strlen(actor_id)); - href = xs_fmt("%s/people#%s", user->actor, md5); + href = xs_fmt("%s/people/%s", user->actor, md5); } } @@ -2355,7 +2355,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, } if (!read_only && (fwers || fwing)) - href = xs_fmt("%s/people#%s", user->actor, p); + href = xs_fmt("%s/people/%s", user->actor, p); else href = xs_dup(id); @@ -3856,6 +3856,29 @@ xs_str *html_people(snac *user) return xs_html_render_s(html, "\n"); } +xs_str *html_people_one(snac *user, const char *actor) +{ + const char *proxy = NULL; + + if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) + proxy = user->actor; + + xs_html *lists = xs_html_tag("div", + xs_html_attr("class", "snac-posts")); + + xs *foll = xs_list_append(xs_list_new(), actor); + + xs_html_add(lists, + html_people_list(user, foll, L("People - single"), "p", proxy)); + + xs_html *html = xs_html_tag("html", + html_user_head(user, NULL, NULL), + xs_html_add(html_user_body(user, 0), + lists, + html_footer(user))); + + return xs_html_render_s(html, "\n"); +} xs_str *html_notifications(snac *user, int skip, int show) { @@ -4415,7 +4438,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, actor_add(actor, actor_obj); /* create a people list with only one element */ - l = xs_list_append(xs_list_new(), actor); + l = xs_list_append(l, actor); xs *title = xs_fmt(L("Search results for account %s"), q); @@ -4555,6 +4578,30 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else + if (xs_startswith(p_path, "people/")) { /** the list of people **/ + if (!login(&snac, req)) { + *body = xs_dup(uid); + status = HTTP_STATUS_UNAUTHORIZED; + } + else { + xs *actor_dict = NULL; + const char *actor_id = NULL; + xs *actor = NULL; + + if (valid_status(object_get_by_md5(p_path + strlen("people/"), &actor_dict)) && + (actor_id = xs_dict_get(actor_dict, "id")) != NULL && + valid_status(actor_get(actor_id, &actor))) { + *body = html_people_one(&snac, actor_id); + *b_size = strlen(*body); + status = HTTP_STATUS_OK; + } + else { + *body = xs_dup(uid); + status = HTTP_STATUS_NOT_FOUND; + } + } + } + else if (strcmp(p_path, "notifications") == 0) { /** the list of notifications **/ if (!login(&snac, req)) { *body = xs_dup(uid); -- cgit v1.2.3 From 93f82fae9e024ae4a7338701db762e390056e990 Mon Sep 17 00:00:00 2001 From: Alexandre Oliva Date: Tue, 30 Dec 2025 13:12:46 +0100 Subject: Add posts by actor to people page Select posts by the actor from the given timeline range. Since posts by the actor may be very sparse in the timeline, add a "More (x 10)" button to bump the show count, that controls how many timeline posts we'll filter *from*. We could conceivably keep searching the timeline until we find as many posts as requested or reach the end, but that could take a very long time. Just filtering the given ranges is much simpler, and probably sufficiently intuitive despite the potential initial surprise. --- html.c | 85 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 78 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index c03c867..70c599f 100644 --- a/html.c +++ b/html.c @@ -3856,25 +3856,89 @@ xs_str *html_people(snac *user) return xs_html_render_s(html, "\n"); } -xs_str *html_people_one(snac *user, const char *actor) +/* Filter list to display only posts by actor. We'll probably show + fewer than show posts. Should we try harder to find some? */ +xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, + int skip, int show, int show_more, const char *page) { const char *proxy = NULL; + xs_list *p = (xs_list *)list; + const char *v; if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) proxy = user->actor; + xs_html *body = html_user_body(user, 0); + xs_html *lists = xs_html_tag("div", xs_html_attr("class", "snac-posts")); xs *foll = xs_list_append(xs_list_new(), actor); xs_html_add(lists, - html_people_list(user, foll, L("People - single"), "p", proxy)); + html_people_list(user, foll, L("Contact's posts"), "p", proxy)); + + xs_html_add(body, lists); + + while (xs_list_iter(&p, &v)) { + xs *msg = NULL; + int status; + + status = timeline_get_by_md5(user, v, &msg); + + if (!valid_status(status)) + continue; + + const char *by = xs_dict_get(msg, "attributedTo"); + if (!by || strcmp(actor, by) != 0) + continue; + + xs_html *entry = html_entry(user, msg, 0, 0, v, 1); + + if (entry != NULL) + xs_html_add(lists, + entry); + } + + if (show_more) { + xs *m = NULL; + xs *m10 = NULL; + xs *ss = xs_fmt("skip=%d&show=%d", skip + show, show); + + xs *url = xs_dup(user == NULL ? srv_baseurl : user->actor); + + if (page != NULL) + url = xs_str_cat(url, page); + + if (xs_str_in(url, "?") != -1) + m = xs_fmt("%s&%s", url, ss); + else + m = xs_fmt("%s?%s", url, ss); + m10 = xs_fmt("%s0", m); + + xs_html *more_links = xs_html_tag("p", + xs_html_tag("a", + xs_html_attr("href", url), + xs_html_attr("name", "snac-more"), + xs_html_text(L("Back to top"))), + xs_html_text(" - "), + xs_html_tag("a", + xs_html_attr("href", m), + xs_html_attr("name", "snac-more"), + xs_html_text(L("More..."))), + xs_html_text(" - "), + xs_html_tag("a", + xs_html_attr("href", m10), + xs_html_attr("name", "snac-more"), + xs_html_text(L("More (x 10)...")))); + + xs_html_add(body, + more_links); + } xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), - xs_html_add(html_user_body(user, 0), - lists, + xs_html_add(body, html_footer(user))); return xs_html_render_s(html, "\n"); @@ -4578,7 +4642,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, } } else - if (xs_startswith(p_path, "people/")) { /** the list of people **/ + if (xs_startswith(p_path, "people/")) { /** a single actor **/ if (!login(&snac, req)) { *body = xs_dup(uid); status = HTTP_STATUS_UNAUTHORIZED; @@ -4587,11 +4651,18 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *actor_dict = NULL; const char *actor_id = NULL; xs *actor = NULL; + xs_list *page_lst = xs_split_n(p_path, "?", 2); + xs *page = xs_str_cat(xs_str_new("/"), xs_list_get(page_lst, 0)); + xs_list *l = xs_split_n(page, "/", 3); + const char *actor_md5 = xs_list_get(l, 2); - if (valid_status(object_get_by_md5(p_path + strlen("people/"), &actor_dict)) && + if (valid_status(object_get_by_md5(actor_md5, &actor_dict)) && (actor_id = xs_dict_get(actor_dict, "id")) != NULL && valid_status(actor_get(actor_id, &actor))) { - *body = html_people_one(&snac, actor_id); + int more = 0; + xs *list = timeline_list(&snac, "private", skip, show, &more); + + *body = html_people_one(&snac, actor_id, list, skip, show, more, page); *b_size = strlen(*body); status = HTTP_STATUS_OK; } -- cgit v1.2.3 From f99895032e33d5c9a80c45bae50fa433f4ea1c48 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 30 Dec 2025 13:14:50 +0100 Subject: Fixed indentation in previous patch (my fault). --- html.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 70c599f..223cf4c 100644 --- a/html.c +++ b/html.c @@ -3914,7 +3914,8 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, m = xs_fmt("%s&%s", url, ss); else m = xs_fmt("%s?%s", url, ss); - m10 = xs_fmt("%s0", m); + + m10 = xs_fmt("%s0", m); xs_html *more_links = xs_html_tag("p", xs_html_tag("a", -- cgit v1.2.3 From 20eba6f5192176ad0fc3c468cd4bd8dda62f15e6 Mon Sep 17 00:00:00 2001 From: Alexandre Oliva Date: Thu, 1 Jan 2026 16:25:00 +0100 Subject: Add boosts, likes and reacts to actor's people page Show in an actor's page anything that the user could have seen from actor in timeline or notifications, namely: - posts by actor (use get_atto to identify the actor) - boosts by actor - user's posts with likes or emojireacts by actor That said, in this view, only the latest boost is shown for a post, so it might not seem like the boost is by actor. Likes and emojireacts aren't even shown, so the reason why a post appears might be puzzling. Use timeline_simple_list, since we don't show entire conversations, and we want to identify all posts with actor's interactions. Saturate show at max_timeline_entries, so that we don't silently skip entries. --- html.c | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 223cf4c..3e1d7ba 100644 --- a/html.c +++ b/html.c @@ -3889,8 +3889,22 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, if (!valid_status(status)) continue; - const char *by = xs_dict_get(msg, "attributedTo"); - if (!by || strcmp(actor, by) != 0) + const char *id = xs_dict_get(msg, "id"); + const char *by = get_atto(msg); + xs *actor_md5 = NULL; + xs_list *boosts = NULL; + xs_list *likes = NULL; + xs_list *reacts = NULL; + /* Besides actor's posts, also show actor's boosts, and also + posts by user with likes or reacts by actor. I.e., any + actor's actions that user could have seen in the timeline + or in notifications. */ + if (!(by && strcmp(actor, by) == 0) && + xs_list_in((boosts = object_announces(id)), + (actor_md5 = xs_md5_hex(actor, strlen(actor)))) == -1 && + (!(by && strcmp(user->actor, by) == 0) || + (xs_list_in((likes = object_likes(id)), actor_md5) == -1 && + xs_list_in((reacts = object_get_emoji_reacts(id)), actor_md5) == -1))) continue; xs_html *entry = html_entry(user, msg, 0, 0, v, 1); @@ -4359,8 +4373,12 @@ int html_get_handler(const xs_dict *req, const char *q_path, cache = 0; int skip = 0; + const char *max_show_default = "50"; + int max_show = xs_number_get(xs_dict_get_def(srv_config, "max_timeline_entries", + max_show_default)); int def_show = xs_number_get(xs_dict_get_def(srv_config, "def_timeline_entries", - xs_dict_get_def(srv_config, "max_timeline_entries", "50"))); + xs_dict_get_def(srv_config, "max_timeline_entries", + max_show_default))); int show = def_show; if ((v = xs_dict_get(q_vars, "skip")) != NULL) @@ -4386,6 +4404,8 @@ int html_get_handler(const xs_dict *req, const char *q_path, /* a show of 0 has no sense */ if (show == 0) show = def_show; + if (show > max_show) + show = max_show; if (p_path == NULL) { /** public timeline **/ xs *h = xs_str_localtime(0, "%Y-%m.html"); @@ -4661,7 +4681,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, (actor_id = xs_dict_get(actor_dict, "id")) != NULL && valid_status(actor_get(actor_id, &actor))) { int more = 0; - xs *list = timeline_list(&snac, "private", skip, show, &more); + xs *list = timeline_simple_list(&snac, "private", skip, show, &more); *body = html_people_one(&snac, actor_id, list, skip, show, more, page); *b_size = strlen(*body); -- cgit v1.2.3 From 11af00194e3e0ec15e17a23556dc2929f92e0210 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 1 Jan 2026 17:01:03 +0100 Subject: Bumped copyright year. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 3e1d7ba..ef8816d 100644 --- a/html.c +++ b/html.c @@ -1,5 +1,5 @@ /* snac - A simple, minimalistic ActivityPub instance */ -/* copyright (c) 2022 - 2025 grunfink et al. / MIT license */ +/* copyright (c) 2022 - 2026 grunfink et al. / MIT license */ #include "xs.h" #include "xs_io.h" -- cgit v1.2.3 From 6c876107aba84d2c066b62929ab094bf6d92ef71 Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 6 Jan 2026 14:26:57 +0100 Subject: Hide EmojiReacts from muted actors and blocked instances. --- html.c | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index ef8816d..d5a59d8 100644 --- a/html.c +++ b/html.c @@ -2460,6 +2460,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *content = xs_dict_get(m, "content"); const char *actor = xs_dict_get(m, "actor"); const xs_list *contentl = xs_dict_get(sfrl, content); + + if (is_muted(user, actor) || is_instance_blocked(actor)) + continue; + xs *actors = xs_list_new(); actors = xs_list_append(actors, actor); char me = actor && user && strcmp(actor, user->actor) == 0; -- cgit v1.2.3 From a01e4b148fd32a015167478171443047507c168d Mon Sep 17 00:00:00 2001 From: grunfink Date: Tue, 6 Jan 2026 18:53:38 +0100 Subject: Fixed crash. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index d5a59d8..4299396 100644 --- a/html.c +++ b/html.c @@ -2461,7 +2461,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, const char *actor = xs_dict_get(m, "actor"); const xs_list *contentl = xs_dict_get(sfrl, content); - if (is_muted(user, actor) || is_instance_blocked(actor)) + if ((user && is_muted(user, actor)) || is_instance_blocked(actor)) continue; xs *actors = xs_list_new(); -- cgit v1.2.3 From c6642e958b628426f2c7b0d71b427a7960cf063c Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 8 Jan 2026 08:43:50 +0100 Subject: Don't show any EmojiReactions if they are disabled. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index 4299396..c41d7f4 100644 --- a/html.c +++ b/html.c @@ -2447,7 +2447,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, /* add all emoji reacts */ int is_emoji = 0; - { + if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { int c = 0; const xs_dict *k; xs *ls = xs_list_new(); -- cgit v1.2.3 From 5ef0483ab34892d7ea773a690a7c17e3d102cf46 Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 8 Jan 2026 08:47:57 +0100 Subject: Don't show the EmojiReact dropdown if disabled. --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index c41d7f4..f26ac4d 100644 --- a/html.c +++ b/html.c @@ -2103,7 +2103,7 @@ xs_html *html_entry_controls(snac *user, const char *actor, xs_html_tag("p", NULL)); } - { /** emoji react **/ + if (!xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) { /** emoji react **/ /* the post textarea */ xs *div_id = xs_fmt("%s_reply", md5); xs *form_id = xs_fmt("%s_reply_form", md5); -- cgit v1.2.3 From a8cbbeeb3fdc8ec660270b9112b50d2dee71e55d Mon Sep 17 00:00:00 2001 From: grunfink Date: Thu, 8 Jan 2026 09:05:53 +0100 Subject: Also disable EmojiReact notifications, if so configured. --- html.c | 3 +++ 1 file changed, 3 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index f26ac4d..d5294cb 100644 --- a/html.c +++ b/html.c @@ -4030,6 +4030,9 @@ xs_str *html_notifications(snac *user, int skip, int show) if (xs_is_string(id2) && xs_set_add(&rep, id2) != 1) continue; + if (strcmp(type, "EmojiReact") == 0 && xs_is_true(xs_dict_get(srv_config, "disable_emojireact"))) + continue; + object_get(id, &obj); const char *msg_id = NULL; -- cgit v1.2.3 From 6ad2365a41dd84ee925d7921a1c5756499d6fb17 Mon Sep 17 00:00:00 2001 From: byte Date: Mon, 12 Jan 2026 01:25:49 +0100 Subject: user-specified word mutes and matching --- html.c | 95 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index d5294cb..ebd9331 100644 --- a/html.c +++ b/html.c @@ -16,6 +16,7 @@ #include "xs_url.h" #include "xs_random.h" #include "xs_http.h" +#include "xs_list_tools.h" #include "snac.h" @@ -1864,6 +1865,38 @@ xs_html *html_top_controls(snac *user) xs_html_attr("class", "button"), xs_html_attr("value", L("Update hashtags"))))))); + xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor); + xs *muted_words = xs_join(xs_dict_get_def(user->config, + "muted_words", xs_stock(XSTYPE_LIST)), "\n"); + + xs_html_add(top_controls, + xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(L("Muted words..."))), + xs_html_tag("p", + xs_html_text(L("One word per line, partial matches count"))), + xs_html_tag("div", + xs_html_attr("class", "snac-muted-words"), + xs_html_tag("form", + xs_html_attr("autocomplete", "off"), + xs_html_attr("method", "post"), + xs_html_attr("action", muted_words_action), + xs_html_attr("enctype", "multipart/form-data"), + + xs_html_tag("textarea", + xs_html_attr("name", "muted_words"), + xs_html_attr("cols", "40"), + xs_html_attr("rows", "4"), + xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"), + xs_html_text(muted_words)), + + xs_html_tag("br", NULL), + + xs_html_sctag("input", + xs_html_attr("type", "submit"), + xs_html_attr("class", "button"), + xs_html_attr("value", L("Update muted words"))))))); + return top_controls; } @@ -2144,6 +2177,30 @@ xs_html *html_entry_controls(snac *user, const char *actor, } +static const xs_str* words_in_content(const xs_list *words, const xs_val *content) +/* returns a word that matches any of the words in content */ +{ + if (!xs_is_list(words) || !xs_is_string(content)) { + return NULL; + } + xs *c = xs_split(content, " "); + xs *sc = xs_list_sort(c, NULL); + + const xs_str *wv; + const xs_str *cv; + xs_list_foreach(words, wv) { + xs_list_foreach(sc, cv) { + xs_tolower_i((xs_str*)cv); + if(xs_str_in(cv, wv) != -1){ + return wv; + } + } + } + + return NULL; +} + + xs_html *html_entry(snac *user, xs_dict *msg, int read_only, int level, const char *md5, int hide_children) { @@ -2438,6 +2495,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only, xs_html_text(v), xs_html_text(L(" [SENSITIVE CONTENT]")))); } + else + if (user && + /* muted_words is all lowercase and sorted for performance */ + (v = words_in_content(xs_dict_get(user->config, "muted_words"), + xs_dict_get(msg, "content"))) != NULL) { + snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id)); + snac_content = xs_html_tag("details", + xs_html_tag("summary", + xs_html_text(L("Muted: ")), + xs_html_text(v))); + } else { snac_content = xs_html_tag("div", NULL); } @@ -5813,6 +5881,33 @@ int html_post_handler(const xs_dict *req, const char *q_path, status = HTTP_STATUS_SEE_OTHER; } + else + if (p_path && strcmp(p_path, "admin/muted-words") == 0) { + const char *words = xs_dict_get(p_vars, "muted_words"); + + if (xs_is_string(words)) { + xs *new_words = xs_list_new(); + xs *l = xs_split(words, "\n"); + const char *v; + + xs_list_foreach(l, v) { + xs *s1 = xs_strip_i(xs_dup(v)); + s1 = xs_replace_i(s1, " ", ""); + + if (*s1 == '\0') + continue; + + xs *s2 = xs_utf8_to_lower(s1); + + new_words = xs_list_insert_sorted(new_words, s2); + } + + snac.config = xs_dict_set(snac.config, "muted_words", new_words); + user_persist(&snac, 0); + } + + status = HTTP_STATUS_SEE_OTHER; + } if (status == HTTP_STATUS_SEE_OTHER) { const char *hard_redir = xs_dict_get(p_vars, "hard-redir"); -- cgit v1.2.3 From f5ad346f7d819a1fe71ac6f1bc98e16fe1aead01 Mon Sep 17 00:00:00 2001 From: violette Date: Mon, 12 Jan 2026 05:09:21 +0100 Subject: Use the metadata pronouns. --- html.c | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) (limited to 'html.c') diff --git a/html.c b/html.c index ebd9331..9aaaa50 100644 --- a/html.c +++ b/html.c @@ -162,6 +162,31 @@ xs_str *actor_name(xs_dict *actor, const char *proxy) } +xs_str *actor_pronouns(xs_dict *actor) +/* gets the actor name */ +{ + const xs_list *attachment; + const xs_dict *d; + const char *pronouns = ""; + char *ret; + + if (!xs_is_null((attachment = xs_dict_get(actor, "attachment")))) { + xs_list_foreach(attachment, d) { + char *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); + /* make sure that we are reading the correct metadata */ + if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) + pronouns = xs_dict_get(d, "value"); + } + } + + /*

breaks page, cannot nest them */ + ret = xs_replace_i(xs_dup(pronouns), "

", ""); + ret = xs_replace_i(ret, "

", ""); + + return ret; +} + + xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy) /* needed when we have local text with no tags attached */ { @@ -195,6 +220,10 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, int fwer = 0; xs *name = actor_name(actor, proxy); + xs *pronouns = actor_pronouns(actor); + char pronouns_c = 0; + if (*pronouns != '\0') + pronouns_c = 1; /* get the avatar */ if ((v = xs_dict_get(actor, "icon")) != NULL) { @@ -251,6 +280,15 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs_html_text("»"))); } + if (pronouns_c > 0) + xs_html_add(actor_icon, + xs_html_text(" "), + xs_html_tag("span", + xs_html_attr("class", "snac-pronouns"), + xs_html_attr("title", "user's pronouns"), + xs_html_raw(pronouns))); + + if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { xs_html_add(actor_icon, xs_html_text(" "), -- cgit v1.2.3 From b1612de2102d9c30bf42402f38ffdc52ab482674 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 05:11:45 +0100 Subject: Minor tweak. --- html.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 9aaaa50..9e96108 100644 --- a/html.c +++ b/html.c @@ -168,14 +168,16 @@ xs_str *actor_pronouns(xs_dict *actor) const xs_list *attachment; const xs_dict *d; const char *pronouns = ""; - char *ret; + xs_str *ret; - if (!xs_is_null((attachment = xs_dict_get(actor, "attachment")))) { + if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) { xs_list_foreach(attachment, d) { - char *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); + xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); /* make sure that we are reading the correct metadata */ - if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) + if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) { pronouns = xs_dict_get(d, "value"); + break; + } } } -- cgit v1.2.3 From 7fe8761ed7992762603c01610a115fde199dcaaf Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 05:20:52 +0100 Subject: Strip all HTML tags from pronouns. --- html.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 9e96108..2107609 100644 --- a/html.c +++ b/html.c @@ -181,9 +181,8 @@ xs_str *actor_pronouns(xs_dict *actor) } } - /*

breaks page, cannot nest them */ - ret = xs_replace_i(xs_dup(pronouns), "

", ""); - ret = xs_replace_i(ret, "

", ""); + /* strip all HTML tags */ + ret = xs_regex_replace(pronouns, "]+>", ""); return ret; } -- cgit v1.2.3 From a84e74e8f22e0be502d1b4b98cadaf284db5af23 Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 06:14:58 +0100 Subject: Move the pronouns closer to the account name. --- html.c | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 2107609..2ff6f79 100644 --- a/html.c +++ b/html.c @@ -222,9 +222,6 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs *name = actor_name(actor, proxy); xs *pronouns = actor_pronouns(actor); - char pronouns_c = 0; - if (*pronouns != '\0') - pronouns_c = 1; /* get the avatar */ if ((v = xs_dict_get(actor, "icon")) != NULL) { @@ -259,16 +256,28 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, if (href == NULL) href = xs_dup(actor_id); + xs_html *name_link = xs_html_tag("a", + xs_html_attr("href", href), + xs_html_attr("class", "p-author h-card snac-author"), + xs_html_raw(name)); /* name is already html-escaped */ + + if (*pronouns) { + xs_html_add(name_link, + xs_html_text(" ["), + xs_html_tag("span", + xs_html_attr("class", "snac-pronouns"), + xs_html_attr("title", "user's pronouns"), + xs_html_raw(pronouns)), + xs_html_text("]")); + } + xs_html_add(actor_icon, xs_html_sctag("img", xs_html_attr("loading", "lazy"), xs_html_attr("class", "snac-avatar"), xs_html_attr("src", avatar), xs_html_attr("alt", "[?]")), - xs_html_tag("a", - xs_html_attr("href", href), - xs_html_attr("class", "p-author h-card snac-author"), - xs_html_raw(name))); /* name is already html-escaped */ + name_link); if (!xs_is_null(url)) { xs *md5 = xs_md5_hex(url, strlen(url)); @@ -281,14 +290,6 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, xs_html_text("»"))); } - if (pronouns_c > 0) - xs_html_add(actor_icon, - xs_html_text(" "), - xs_html_tag("span", - xs_html_attr("class", "snac-pronouns"), - xs_html_attr("title", "user's pronouns"), - xs_html_raw(pronouns))); - if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { xs_html_add(actor_icon, -- cgit v1.2.3 From 3f3e4892ff0d0c46b49cae88e7e74ba5c6c11a3b Mon Sep 17 00:00:00 2001 From: grunfink Date: Mon, 12 Jan 2026 18:08:34 +0100 Subject: Fixed typo. --- html.c | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 2ff6f79..a11b448 100644 --- a/html.c +++ b/html.c @@ -182,8 +182,7 @@ xs_str *actor_pronouns(xs_dict *actor) } /* strip all HTML tags */ - ret = xs_regex_replace(pronouns, "]+>", ""); - + ret = xs_regex_replace(pronouns, "]+>", ""); return ret; } -- cgit v1.2.3 From 485986dab9ab0ee59c5829f85c6b066f6bfa3d72 Mon Sep 17 00:00:00 2001 From: byte Date: Fri, 16 Jan 2026 21:11:42 +0000 Subject: simple cosmetic counters --- html.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a11b448..86620fb 100644 --- a/html.c +++ b/html.c @@ -3710,7 +3710,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c xs_html *people = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), - xs_html_text(header)), + xs_html_raw(xs_fmt("%s - %d\n", header, xs_list_len(list)))), snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", -- cgit v1.2.3 From 1719c1fe95017faa1a75ba58c5391e05eb2e772e Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 17 Jan 2026 02:45:33 +0000 Subject: doing count conditional --- html.c | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 86620fb..c834fcf 100644 --- a/html.c +++ b/html.c @@ -3704,13 +3704,19 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, } -xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy) +xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int do_count) { xs_html *snac_posts; + const char *header_cnt; + if (do_count) + header_cnt = xs_fmt("%s - %d\n", header, xs_list_len(list)); + else + header_cnt = xs_fmt("%s\n", header); + xs_html *people = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), - xs_html_raw(xs_fmt("%s - %d\n", header, xs_list_len(list)))), + xs_html_raw(header_cnt)), snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", @@ -3951,12 +3957,12 @@ xs_str *html_people(snac *user) if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { xs_html_add(lists, - html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); + html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, 1)); } xs_html_add(lists, - html_people_list(user, wing, L("People you follow"), "i", proxy), - html_people_list(user, wers, L("People that follow you"), "e", proxy)); + html_people_list(user, wing, L("People you follow"), "i", proxy, 1), + html_people_list(user, wers, L("People that follow you"), "e", proxy, 1)); xs_html *html = xs_html_tag("html", html_user_head(user, NULL, NULL), @@ -3987,7 +3993,7 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list, xs *foll = xs_list_append(xs_list_new(), actor); xs_html_add(lists, - html_people_list(user, foll, L("Contact's posts"), "p", proxy)); + html_people_list(user, foll, L("Contact's posts"), "p", proxy, 0)); xs_html_add(body, lists); @@ -4641,7 +4647,7 @@ int html_get_handler(const xs_dict *req, const char *q_path, xs *title = xs_fmt(L("Search results for account %s"), q); - page = html_people_list(&snac, l, title, "wf", NULL); + page = html_people_list(&snac, l, title, "wf", NULL, 1); } } -- cgit v1.2.3 From 84c0ee63cf4a67e7fb8484ef874b121160b63382 Mon Sep 17 00:00:00 2001 From: byte Date: Sat, 17 Jan 2026 02:49:56 +0000 Subject: segfault fix --- html.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'html.c') diff --git a/html.c b/html.c index a11b448..b4cb1c7 100644 --- a/html.c +++ b/html.c @@ -167,6 +167,7 @@ xs_str *actor_pronouns(xs_dict *actor) { const xs_list *attachment; const xs_dict *d; + const char *v; const char *pronouns = ""; xs_str *ret; @@ -175,7 +176,9 @@ xs_str *actor_pronouns(xs_dict *actor) xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name")); /* make sure that we are reading the correct metadata */ if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) { - pronouns = xs_dict_get(d, "value"); + /* safeguard from NULL values */ + v = xs_dict_get(d, "value"); + pronouns = v ? v : pronouns; break; } } -- cgit v1.2.3 From 8ce4bd725dc9c37a58a1924a2ba355c176a2dd4a Mon Sep 17 00:00:00 2001 From: grunfink Date: Sat, 17 Jan 2026 05:24:50 +0100 Subject: Fixed small memory leak. --- html.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'html.c') diff --git a/html.c b/html.c index 1a25eaa..3f5435c 100644 --- a/html.c +++ b/html.c @@ -3710,7 +3710,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only, xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int do_count) { xs_html *snac_posts; - const char *header_cnt; + xs *header_cnt; if (do_count) header_cnt = xs_fmt("%s - %d\n", header, xs_list_len(list)); else @@ -3719,7 +3719,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c xs_html *people = xs_html_tag("div", xs_html_tag("h2", xs_html_attr("class", "snac-header"), - xs_html_raw(header_cnt)), + xs_html_text(header_cnt)), snac_posts = xs_html_tag("details", xs_html_attr("open", NULL), xs_html_tag("summary", -- cgit v1.2.3