summaryrefslogtreecommitdiff
path: root/html.c
diff options
context:
space:
mode:
Diffstat (limited to 'html.c')
-rw-r--r--html.c454
1 files changed, 355 insertions, 99 deletions
diff --git a/html.c b/html.c
index d5294cb..36d39c4 100644
--- a/html.c
+++ b/html.c
@@ -16,6 +16,7 @@
16#include "xs_url.h" 16#include "xs_url.h"
17#include "xs_random.h" 17#include "xs_random.h"
18#include "xs_http.h" 18#include "xs_http.h"
19#include "xs_list_tools.h"
19 20
20#include "snac.h" 21#include "snac.h"
21 22
@@ -161,6 +162,34 @@ xs_str *actor_name(xs_dict *actor, const char *proxy)
161} 162}
162 163
163 164
165xs_str *actor_pronouns(xs_dict *actor)
166/* gets the actor name */
167{
168 const xs_list *attachment;
169 const xs_dict *d;
170 const char *v;
171 const char *pronouns = "";
172 xs_str *ret;
173
174 if (xs_is_list((attachment = xs_dict_get(actor, "attachment")))) {
175 xs_list_foreach(attachment, d) {
176 xs *prop = xs_utf8_to_lower(xs_dict_get(d, "name"));
177 /* make sure that we are reading the correct metadata */
178 if (strlen(prop) == 8 && strcmp(prop, "pronouns") == 0) {
179 /* safeguard from NULL values */
180 v = xs_dict_get(d, "value");
181 pronouns = v ? v : pronouns;
182 break;
183 }
184 }
185 }
186
187 /* strip all HTML tags */
188 ret = xs_regex_replace(pronouns, "</?[^>]+>", "");
189 return ret;
190}
191
192
164xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy) 193xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy)
165/* needed when we have local text with no tags attached */ 194/* needed when we have local text with no tags attached */
166{ 195{
@@ -181,6 +210,41 @@ xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char
181} 210}
182 211
183 212
213xs_str *html_date_label(snac *user, const char *date)
214{
215 time_t t;
216
217 /* check if a user has actually set a timezone */
218 if (user != NULL && xs_dict_get(user->config, "tz") != NULL &&
219 (t = xs_parse_iso_date(date, 0)) != 0) {
220 t += xs_tz_offset(user->tz);
221
222 time_t today = time(NULL);
223 if (today - t >= 3600 * 24 * 30 * 6) {
224 /* more than half a year ago */
225 return xs_str_utctime(t, "%Y-%m-%d %H:%M");
226 }
227
228 xs_str *date = xs_str_utctime(t, "%b%d %H:%M");
229
230 struct tm tm;
231 localtime_r(&t, &tm);
232
233 if (tm.tm_mon >= 0 && tm.tm_mon <= 11) {
234 /* copy the 3letter translated month name */
235 const char *m = L(months[tm.tm_mon]);
236
237 if (xs_is_string(m))
238 date = xs_replace_i(date, months[tm.tm_mon], m);
239 }
240
241 return date;
242 }
243
244 return xs_crop_i(xs_dup(date), 0, 10);
245}
246
247
184xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, 248xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
185 const char *udate, const char *url, int scope, 249 const char *udate, const char *url, int scope,
186 int in_people, const char *proxy, const char *lang, 250 int in_people, const char *proxy, const char *lang,
@@ -194,6 +258,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
194 int fwer = 0; 258 int fwer = 0;
195 259
196 xs *name = actor_name(actor, proxy); 260 xs *name = actor_name(actor, proxy);
261 xs *pronouns = actor_pronouns(actor);
197 262
198 /* get the avatar */ 263 /* get the avatar */
199 if ((v = xs_dict_get(actor, "icon")) != NULL) { 264 if ((v = xs_dict_get(actor, "icon")) != NULL) {
@@ -209,6 +274,14 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
209 avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64()); 274 avatar = xs_fmt("data:image/png;base64, %s", default_avatar_base64());
210 275
211 const char *actor_id = xs_dict_get(actor, "id"); 276 const char *actor_id = xs_dict_get(actor, "id");
277 const char *html_url = xs_dict_get_def(actor, "url", actor_id);
278
279 if (xs_is_list(html_url))
280 html_url = xs_list_get(html_url, 0);
281
282 if (!xs_is_string(html_url))
283 html_url = actor_id;
284
212 xs *href = NULL; 285 xs *href = NULL;
213 286
214 if (user) { 287 if (user) {
@@ -217,16 +290,27 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
217 } 290 }
218 291
219 if (user && !in_people) { 292 if (user && !in_people) {
220 /* if this actor is a follower or being followed, create an 293 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
221 anchored link to the people page instead of the actor url */ 294 href = xs_fmt("%s/people/%s", user->actor, md5);
222 if (fwer || fwing) {
223 xs *md5 = xs_md5_hex(actor_id, strlen(actor_id));
224 href = xs_fmt("%s/people/%s", user->actor, md5);
225 }
226 } 295 }
227 296
228 if (href == NULL) 297 if (href == NULL)
229 href = xs_dup(actor_id); 298 href = xs_dup(html_url);
299
300 xs_html *name_link = xs_html_tag("a",
301 xs_html_attr("href", href),
302 xs_html_attr("class", "p-author h-card snac-author"),
303 xs_html_raw(name)); /* name is already html-escaped */
304
305 if (*pronouns) {
306 xs_html_add(name_link,
307 xs_html_text(" ["),
308 xs_html_tag("span",
309 xs_html_attr("class", "snac-pronouns"),
310 xs_html_attr("title", "user's pronouns"),
311 xs_html_raw(pronouns)),
312 xs_html_text("]"));
313 }
230 314
231 xs_html_add(actor_icon, 315 xs_html_add(actor_icon,
232 xs_html_sctag("img", 316 xs_html_sctag("img",
@@ -234,10 +318,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
234 xs_html_attr("class", "snac-avatar"), 318 xs_html_attr("class", "snac-avatar"),
235 xs_html_attr("src", avatar), 319 xs_html_attr("src", avatar),
236 xs_html_attr("alt", "[?]")), 320 xs_html_attr("alt", "[?]")),
237 xs_html_tag("a", 321 name_link);
238 xs_html_attr("href", href),
239 xs_html_attr("class", "p-author h-card snac-author"),
240 xs_html_raw(name))); /* name is already html-escaped */
241 322
242 if (!xs_is_null(url)) { 323 if (!xs_is_null(url)) {
243 xs *md5 = xs_md5_hex(url, strlen(url)); 324 xs *md5 = xs_md5_hex(url, strlen(url));
@@ -250,6 +331,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
250 xs_html_text("»"))); 331 xs_html_text("»")));
251 } 332 }
252 333
334
253 if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) { 335 if (strcmp(xs_dict_get(actor, "type"), "Service") == 0) {
254 xs_html_add(actor_icon, 336 xs_html_add(actor_icon,
255 xs_html_text(" "), 337 xs_html_text(" "),
@@ -318,11 +400,11 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
318 xs_html_raw("&nbsp;")); 400 xs_html_raw("&nbsp;"));
319 } 401 }
320 else { 402 else {
321 xs *date_label = xs_crop_i(xs_dup(date), 0, 10); 403 xs *date_label = html_date_label(user, date);
322 xs *date_title = xs_dup(date); 404 xs *date_title = xs_dup(date);
323 405
324 if (!xs_is_null(udate)) { 406 if (!xs_is_null(udate)) {
325 xs *sd = xs_crop_i(xs_dup(udate), 0, 10); 407 xs *sd = html_date_label(user, udate);
326 408
327 date_label = xs_str_cat(date_label, " / ", sd); 409 date_label = xs_str_cat(date_label, " / ", sd);
328 410
@@ -379,7 +461,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
379 xs_html_add(actor_icon, 461 xs_html_add(actor_icon,
380 xs_html_sctag("br", NULL), 462 xs_html_sctag("br", NULL),
381 xs_html_tag("a", 463 xs_html_tag("a",
382 xs_html_attr("href", xs_dict_get(actor, "id")), 464 xs_html_attr("href", html_url),
383 xs_html_attr("class", "p-author-tag h-card snac-author-tag"), 465 xs_html_attr("class", "p-author-tag h-card snac-author-tag"),
384 xs_html_text(user))); 466 xs_html_text(user)));
385 } 467 }
@@ -401,9 +483,14 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
401 const char *type = xs_dict_get(msg, "type"); 483 const char *type = xs_dict_get(msg, "type");
402 const int scope = get_msg_visibility(msg); 484 const int scope = get_msg_visibility(msg);
403 485
404 if (xs_match(type, POSTLIKE_OBJECT_TYPE)) 486 if (xs_match(type, POSTLIKE_OBJECT_TYPE)) {
405 url = xs_dict_get(msg, "id"); 487 url = xs_dict_get(msg, "url");
488 if (xs_is_list(url))
489 url = xs_list_get(url, 0);
406 490
491 if (!xs_is_string(url))
492 url = xs_dict_get(msg, "id");
493 }
407 494
408 date = xs_dict_get(msg, "published"); 495 date = xs_dict_get(msg, "published");
409 udate = xs_dict_get(msg, "updated"); 496 udate = xs_dict_get(msg, "updated");
@@ -427,7 +514,7 @@ void html_note_render_visibility(snac* user, xs_html *form, const int scope)
427 514
428 xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: "))); 515 xs_html *paragraph = xs_html_tag("p", xs_html_text(L("Visibility: ")));
429 const int* to_render = scopes[scope]; 516 const int* to_render = scopes[scope];
430 for( int i = 0; to_render[i] != -1; i++ ){ 517 for( int i = 0; to_render[i] != -1; i++ ) {
431 const int scope_i = to_render[i]; 518 const int scope_i = to_render[i];
432 const char* value = scopes_tags[scope_i]; 519 const char* value = scopes_tags[scope_i];
433 const char* name = scopes_names[scope_i]; 520 const char* name = scopes_names[scope_i];
@@ -735,13 +822,18 @@ xs_html *html_note(snac *user, const char *summary,
735 822
736 /* add poll controls */ 823 /* add poll controls */
737 if (poll) { 824 if (poll) {
825 const xs_number *max_options = xs_dict_get(srv_config, "max_poll_options");
826 xs *poll_limit_str = xs_dup(L("Poll options (one per line, up to 8):"));
827 if (max_options != NULL)
828 poll_limit_str = xs_replace_i(poll_limit_str, "8", xs_number_str(max_options));
829
738 xs_html_add(form, 830 xs_html_add(form,
739 xs_html_tag("p", NULL), 831 xs_html_tag("p", NULL),
740 xs_html_tag("details", 832 xs_html_tag("details",
741 xs_html_tag("summary", 833 xs_html_tag("summary",
742 xs_html_text(L("Poll..."))), 834 xs_html_text(L("Poll..."))),
743 xs_html_tag("p", 835 xs_html_tag("p",
744 xs_html_text(L("Poll options (one per line, up to 8):")), 836 xs_html_text(poll_limit_str),
745 xs_html_sctag("br", NULL), 837 xs_html_sctag("br", NULL),
746 xs_html_tag("textarea", 838 xs_html_tag("textarea",
747 xs_html_attr("class", "snac-textarea"), 839 xs_html_attr("class", "snac-textarea"),
@@ -769,7 +861,13 @@ xs_html *html_note(snac *user, const char *summary,
769 xs_html_text(L("End in 1 hour"))), 861 xs_html_text(L("End in 1 hour"))),
770 xs_html_tag("option", 862 xs_html_tag("option",
771 xs_html_attr("value", "86400"), 863 xs_html_attr("value", "86400"),
772 xs_html_text(L("End in 1 day")))))); 864 xs_html_text(L("End in 1 day"))),
865 xs_html_tag("option",
866 xs_html_attr("value", "259200"),
867 xs_html_text(L("End in 3 days"))),
868 xs_html_tag("option",
869 xs_html_attr("value", "31536000"),
870 xs_html_text(L("End in 1 year"))))));
773 } 871 }
774 872
775 xs_html_add(form, 873 xs_html_add(form,
@@ -1346,6 +1444,20 @@ static xs_html *html_user_body(snac *user, int read_only)
1346} 1444}
1347 1445
1348 1446
1447xs_html *html_checkbox(const char *form_name, const char *label, int flag)
1448/* helper for checkbox rendering */
1449{
1450 return xs_html_tag("p",
1451 xs_html_sctag("input",
1452 xs_html_attr("type", "checkbox"),
1453 xs_html_attr("name", form_name),
1454 xs_html_attr(flag ? "checked" : "", NULL)),
1455 xs_html_tag("label",
1456 xs_html_attr("for", form_name),
1457 xs_html_text(label)));
1458}
1459
1460
1349xs_html *html_top_controls(snac *user) 1461xs_html *html_top_controls(snac *user)
1350/* generates the top controls */ 1462/* generates the top controls */
1351{ 1463{
@@ -1669,70 +1781,21 @@ xs_html *html_top_controls(snac *user)
1669 xs_html_attr("type", "number"), 1781 xs_html_attr("type", "number"),
1670 xs_html_attr("name", "purge_days"), 1782 xs_html_attr("name", "purge_days"),
1671 xs_html_attr("value", purge_days))), 1783 xs_html_attr("value", purge_days))),
1672 xs_html_tag("p", 1784 html_checkbox("drop_dm_from_unknown", L("Drop direct messages from people you don't follow"),
1673 xs_html_sctag("input", 1785 xs_is_true(d_dm_f_u)),
1674 xs_html_attr("type", "checkbox"), 1786 html_checkbox("bot", L("This account is a bot"),
1675 xs_html_attr("name", "drop_dm_from_unknown"), 1787 xs_is_true(bot)),
1676 xs_html_attr("id", "drop_dm_from_unknown"), 1788 html_checkbox("auto_boost", L("Auto-boost all mentions to this account"),
1677 xs_html_attr(xs_type(d_dm_f_u) == XSTYPE_TRUE ? "checked" : "", NULL)), 1789 xs_is_true(auto_boost)),
1678 xs_html_tag("label", 1790 html_checkbox("private", L("This account is private "
1679 xs_html_attr("for", "drop_dm_from_unknown"), 1791 "(posts are not shown through the web)"),
1680 xs_html_text(L("Drop direct messages from people you don't follow")))), 1792 xs_is_true(a_private)),
1681 xs_html_tag("p", 1793 html_checkbox("collapse_threads", L("Collapse top threads by default"),
1682 xs_html_sctag("input", 1794 xs_is_true(coll_thrds)),
1683 xs_html_attr("type", "checkbox"), 1795 html_checkbox("approve_followers", L("Follow requests must be approved"),
1684 xs_html_attr("name", "bot"), 1796 xs_is_true(pending)),
1685 xs_html_attr("id", "bot"), 1797 html_checkbox("show_contact_metrics", L("Publish follower and following metrics"),
1686 xs_html_attr(xs_type(bot) == XSTYPE_TRUE ? "checked" : "", NULL)), 1798 xs_is_true(show_foll)),
1687 xs_html_tag("label",
1688 xs_html_attr("for", "bot"),
1689 xs_html_text(L("This account is a bot")))),
1690 xs_html_tag("p",
1691 xs_html_sctag("input",
1692 xs_html_attr("type", "checkbox"),
1693 xs_html_attr("name", "auto_boost"),
1694 xs_html_attr("id", "auto_boost"),
1695 xs_html_attr(xs_is_true(auto_boost) ? "checked" : "", NULL)),
1696 xs_html_tag("label",
1697 xs_html_attr("for", "auto_boost"),
1698 xs_html_text(L("Auto-boost all mentions to this account")))),
1699 xs_html_tag("p",
1700 xs_html_sctag("input",
1701 xs_html_attr("type", "checkbox"),
1702 xs_html_attr("name", "private"),
1703 xs_html_attr("id", "private"),
1704 xs_html_attr(xs_type(a_private) == XSTYPE_TRUE ? "checked" : "", NULL)),
1705 xs_html_tag("label",
1706 xs_html_attr("for", "private"),
1707 xs_html_text(L("This account is private "
1708 "(posts are not shown through the web)")))),
1709 xs_html_tag("p",
1710 xs_html_sctag("input",
1711 xs_html_attr("type", "checkbox"),
1712 xs_html_attr("name", "collapse_threads"),
1713 xs_html_attr("id", "collapse_threads"),
1714 xs_html_attr(xs_is_true(coll_thrds) ? "checked" : "", NULL)),
1715 xs_html_tag("label",
1716 xs_html_attr("for", "collapse_threads"),
1717 xs_html_text(L("Collapse top threads by default")))),
1718 xs_html_tag("p",
1719 xs_html_sctag("input",
1720 xs_html_attr("type", "checkbox"),
1721 xs_html_attr("name", "approve_followers"),
1722 xs_html_attr("id", "approve_followers"),
1723 xs_html_attr(xs_is_true(pending) ? "checked" : "", NULL)),
1724 xs_html_tag("label",
1725 xs_html_attr("for", "approve_followers"),
1726 xs_html_text(L("Follow requests must be approved")))),
1727 xs_html_tag("p",
1728 xs_html_sctag("input",
1729 xs_html_attr("type", "checkbox"),
1730 xs_html_attr("name", "show_contact_metrics"),
1731 xs_html_attr("id", "show_contact_metrics"),
1732 xs_html_attr(xs_is_true(show_foll) ? "checked" : "", NULL)),
1733 xs_html_tag("label",
1734 xs_html_attr("for", "show_contact_metrics"),
1735 xs_html_text(L("Publish follower and following metrics")))),
1736 xs_html_tag("p", 1799 xs_html_tag("p",
1737 xs_html_text(L("Current location:")), 1800 xs_html_text(L("Current location:")),
1738 xs_html_sctag("br", NULL), 1801 xs_html_sctag("br", NULL),
@@ -1864,6 +1927,38 @@ xs_html *html_top_controls(snac *user)
1864 xs_html_attr("class", "button"), 1927 xs_html_attr("class", "button"),
1865 xs_html_attr("value", L("Update hashtags"))))))); 1928 xs_html_attr("value", L("Update hashtags")))))));
1866 1929
1930 xs *muted_words_action = xs_fmt("%s/admin/muted-words", user->actor);
1931 xs *muted_words = xs_join(xs_dict_get_def(user->config,
1932 "muted_words", xs_stock(XSTYPE_LIST)), "\n");
1933
1934 xs_html_add(top_controls,
1935 xs_html_tag("details",
1936 xs_html_tag("summary",
1937 xs_html_text(L("Muted words..."))),
1938 xs_html_tag("p",
1939 xs_html_text(L("One word per line, partial matches count"))),
1940 xs_html_tag("div",
1941 xs_html_attr("class", "snac-muted-words"),
1942 xs_html_tag("form",
1943 xs_html_attr("autocomplete", "off"),
1944 xs_html_attr("method", "post"),
1945 xs_html_attr("action", muted_words_action),
1946 xs_html_attr("enctype", "multipart/form-data"),
1947
1948 xs_html_tag("textarea",
1949 xs_html_attr("name", "muted_words"),
1950 xs_html_attr("cols", "40"),
1951 xs_html_attr("rows", "4"),
1952 xs_html_attr("placeholder", "nascar\nsuperbowl\nFIFA"),
1953 xs_html_text(muted_words)),
1954
1955 xs_html_tag("br", NULL),
1956
1957 xs_html_sctag("input",
1958 xs_html_attr("type", "submit"),
1959 xs_html_attr("class", "button"),
1960 xs_html_attr("value", L("Update muted words")))))));
1961
1867 return top_controls; 1962 return top_controls;
1868} 1963}
1869 1964
@@ -2144,6 +2239,29 @@ xs_html *html_entry_controls(snac *user, const char *actor,
2144} 2239}
2145 2240
2146 2241
2242static const xs_str* words_in_content(const xs_list *words, const xs_val *content)
2243/* returns a word that matches any of the words in content */
2244{
2245 if (!xs_is_list(words) || !xs_is_string(content)) {
2246 return NULL;
2247 }
2248 xs *c = xs_split(content, " ");
2249 xs *sc = xs_list_sort(c, NULL);
2250
2251 const xs_str *wv;
2252 const xs_str *cv;
2253 xs_list_foreach(words, wv) {
2254 xs_list_foreach(sc, cv) {
2255 xs_tolower_i((xs_str*)cv);
2256 if(xs_str_in(cv, wv) != -1)
2257 return wv;
2258 }
2259 }
2260
2261 return NULL;
2262}
2263
2264
2147xs_html *html_entry(snac *user, xs_dict *msg, int read_only, 2265xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2148 int level, const char *md5, int hide_children) 2266 int level, const char *md5, int hide_children)
2149{ 2267{
@@ -2438,6 +2556,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2438 xs_html_text(v), 2556 xs_html_text(v),
2439 xs_html_text(L(" [SENSITIVE CONTENT]")))); 2557 xs_html_text(L(" [SENSITIVE CONTENT]"))));
2440 } 2558 }
2559 else
2560 if (user &&
2561 /* muted_words is all lowercase and sorted for performance */
2562 (v = words_in_content(xs_dict_get(user->config, "muted_words"),
2563 xs_dict_get(msg, "content"))) != NULL) {
2564 snac_debug(user, 1, xs_fmt("word %s muted by user preferences: %s", v, id));
2565 snac_content = xs_html_tag("details",
2566 xs_html_tag("summary",
2567 xs_html_text(L("Muted: ")),
2568 xs_html_text(v)));
2569 }
2441 else { 2570 else {
2442 snac_content = xs_html_tag("div", NULL); 2571 snac_content = xs_html_tag("div", NULL);
2443 } 2572 }
@@ -2675,7 +2804,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2675 xs_html_add(snac_content, 2804 xs_html_add(snac_content,
2676 xs_html_tag("blockquote", 2805 xs_html_tag("blockquote",
2677 xs_html_attr("class", "snac-quoted-post"), 2806 xs_html_attr("class", "snac-quoted-post"),
2678 html_entry(user, quoted_post, 1, 1, NULL, 1))); 2807 html_entry(user, quoted_post, 1, level + 1, NULL, 1)));
2679 } 2808 }
2680 else 2809 else
2681 if (user) 2810 if (user)
@@ -3491,7 +3620,7 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3491 continue; 3620 continue;
3492 3621
3493 const int scope = get_msg_visibility(msg); 3622 const int scope = get_msg_visibility(msg);
3494 if (user != NULL && scope != SCOPE_PUBLIC){ 3623 if (user != NULL && scope != SCOPE_PUBLIC && !is_msg_mine(user, xs_dict_get(msg, "id"))) {
3495 /* is this message a non-public reply? */ 3624 /* is this message a non-public reply? */
3496 const char *irt = get_in_reply_to(msg); 3625 const char *irt = get_in_reply_to(msg);
3497 3626
@@ -3508,13 +3637,11 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3508 } 3637 }
3509 } 3638 }
3510 /* hide non-public posts from /instance view */ 3639 /* hide non-public posts from /instance view */
3511 if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC){ 3640 if (page != NULL && strcmp(page, "/instance") == 0 && scope != SCOPE_PUBLIC)
3512 continue; 3641 continue;
3513 }
3514 /* hide non-public posts viewed from outside */ 3642 /* hide non-public posts viewed from outside */
3515 if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED)) { 3643 if (read_only && (scope != SCOPE_PUBLIC && scope != SCOPE_UNLISTED))
3516 continue; 3644 continue;
3517 }
3518 3645
3519 xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1); 3646 xs_html *entry = html_entry(user, msg, read_only, 0, v, (user && !hide_children) ? 0 : 1);
3520 3647
@@ -3597,13 +3724,19 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
3597} 3724}
3598 3725
3599 3726
3600xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy) 3727xs_html *html_people_list(snac *user, xs_list *list, const char *header, const char *t, const char *proxy, int count)
3601{ 3728{
3602 xs_html *snac_posts; 3729 xs_html *snac_posts;
3730 xs *header_cnt;
3731 if (count > -1)
3732 header_cnt = xs_fmt("%s - %d\n", header, count);
3733 else
3734 header_cnt = xs_fmt("%s\n", header);
3735
3603 xs_html *people = xs_html_tag("div", 3736 xs_html *people = xs_html_tag("div",
3604 xs_html_tag("h2", 3737 xs_html_tag("h2",
3605 xs_html_attr("class", "snac-header"), 3738 xs_html_attr("class", "snac-header"),
3606 xs_html_text(header)), 3739 xs_html_text(header_cnt)),
3607 snac_posts = xs_html_tag("details", 3740 snac_posts = xs_html_tag("details",
3608 xs_html_attr("open", NULL), 3741 xs_html_attr("open", NULL),
3609 xs_html_tag("summary", 3742 xs_html_tag("summary",
@@ -3844,12 +3977,12 @@ xs_str *html_people(snac *user)
3844 3977
3845 if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) { 3978 if (xs_list_len(pending) || xs_is_true(xs_dict_get(user->config, "approve_followers"))) {
3846 xs_html_add(lists, 3979 xs_html_add(lists,
3847 html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy)); 3980 html_people_list(user, pending, L("Pending follow confirmations"), "p", proxy, xs_list_len(pending)));
3848 } 3981 }
3849 3982
3850 xs_html_add(lists, 3983 xs_html_add(lists,
3851 html_people_list(user, wing, L("People you follow"), "i", proxy), 3984 html_people_list(user, wing, L("People you follow"), "i", proxy, following_list_len(user)),
3852 html_people_list(user, wers, L("People that follow you"), "e", proxy)); 3985 html_people_list(user, wers, L("People that follow you"), "e", proxy, xs_list_len(wers)));
3853 3986
3854 xs_html *html = xs_html_tag("html", 3987 xs_html *html = xs_html_tag("html",
3855 html_user_head(user, NULL, NULL), 3988 html_user_head(user, NULL, NULL),
@@ -3869,6 +4002,8 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3869 xs_list *p = (xs_list *)list; 4002 xs_list *p = (xs_list *)list;
3870 const char *v; 4003 const char *v;
3871 4004
4005 enqueue_actor_refresh(user, actor, 0);
4006
3872 if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) 4007 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3873 proxy = user->actor; 4008 proxy = user->actor;
3874 4009
@@ -3880,7 +4015,7 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3880 xs *foll = xs_list_append(xs_list_new(), actor); 4015 xs *foll = xs_list_append(xs_list_new(), actor);
3881 4016
3882 xs_html_add(lists, 4017 xs_html_add(lists,
3883 html_people_list(user, foll, L("Contact's posts"), "p", proxy)); 4018 html_people_list(user, foll, L("Contact's posts"), "p", proxy, -1));
3884 4019
3885 xs_html_add(body, lists); 4020 xs_html_add(body, lists);
3886 4021
@@ -3963,6 +4098,32 @@ xs_str *html_people_one(snac *user, const char *actor, const xs_list *list,
3963 return xs_html_render_s(html, "<!DOCTYPE html>\n"); 4098 return xs_html_render_s(html, "<!DOCTYPE html>\n");
3964} 4099}
3965 4100
4101void notify_filter(snac *user, const xs_dict *p_vars)
4102/* sets filter for notifications */
4103{
4104 const char *v;
4105 int likes_on = (v = xs_dict_get(p_vars, "likes_on")) ? strcmp(v, "on") == 0 : 0;
4106 int reacts_on = (v = xs_dict_get(p_vars, "reacts_on")) ? strcmp(v, "on") == 0 : 0;
4107 int ments_on = (v = xs_dict_get(p_vars, "mentions_on")) ? strcmp(v, "on") == 0 : 0;
4108 int ann_on = (v = xs_dict_get(p_vars, "announces_on")) ? strcmp(v, "on") == 0 : 0;
4109 int foll_on = (v = xs_dict_get(p_vars, "follows_on")) ? strcmp(v, "on") == 0 : 0;
4110 int unfoll_on = (v = xs_dict_get(p_vars, "unfollows_on")) ? strcmp(v, "on") == 0 : 0;
4111 int folreq_on = (v = xs_dict_get(p_vars, "folreqs_on")) ? strcmp(v, "on") == 0 : 0;
4112 int blocks_on = (v = xs_dict_get(p_vars, "blocks_on")) ? strcmp(v, "on") == 0 : 0;
4113 int polls_on = (v = xs_dict_get(p_vars, "polls_on")) ? strcmp(v, "on") == 0 : 0;
4114 xs_dict *filter = xs_dict_new();
4115 filter = xs_dict_set(filter, "likes", xs_stock(likes_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4116 filter = xs_dict_set(filter, "reacts", xs_stock(reacts_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4117 filter = xs_dict_set(filter, "mentions", xs_stock(ments_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4118 filter = xs_dict_set(filter, "announces", xs_stock(ann_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4119 filter = xs_dict_set(filter, "follows", xs_stock(foll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4120 filter = xs_dict_set(filter, "unfollows", xs_stock(unfoll_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4121 filter = xs_dict_set(filter, "folreqs", xs_stock(folreq_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4122 filter = xs_dict_set(filter, "blocks", xs_stock(blocks_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4123 filter = xs_dict_set(filter, "polls", xs_stock(polls_on ? XSTYPE_TRUE : XSTYPE_FALSE));
4124 user->config = xs_dict_set(user->config, "notify_filter", filter);
4125}
4126
3966xs_str *html_notifications(snac *user, int skip, int show) 4127xs_str *html_notifications(snac *user, int skip, int show)
3967{ 4128{
3968 const char *proxy = NULL; 4129 const char *proxy = NULL;
@@ -3970,15 +4131,66 @@ xs_str *html_notifications(snac *user, int skip, int show)
3970 if (xs_is_true(xs_dict_get(srv_config, "proxy_media"))) 4131 if (xs_is_true(xs_dict_get(srv_config, "proxy_media")))
3971 proxy = user->actor; 4132 proxy = user->actor;
3972 4133
3973 xs *n_list = notify_list(user, skip, show); 4134 xs *n_list_unfilt = notify_list(user, skip, show);
3974 xs *n_time = notify_check_time(user, 0); 4135 xs *n_time = notify_check_time(user, 0);
3975 4136
3976 xs_html *body = html_user_body(user, 0); 4137 xs_html *body = html_user_body(user, 0);
4138 const xs_dict *n_filter = xs_dict_get(user->config, "notify_filter");
4139 if (!n_filter) {
4140 user->config = xs_dict_set(user->config, "notify_filter", xs_dict_new());
4141 n_filter = xs_dict_get(user->config, "notify_filter");
4142 }
4143 xs *n_list = notify_filter_list(user, n_list_unfilt);
4144 /* all filters are true by default */
4145 const xs_val *n_def = xs_stock( XSTYPE_TRUE );
4146 int n_likes_on = xs_is_true(xs_dict_get_def(n_filter, "likes", n_def));
4147 int n_reacts_on = xs_is_true(xs_dict_get_def(n_filter, "reacts", n_def));
4148 int n_ments_on = xs_is_true(xs_dict_get_def(n_filter, "mentions", n_def));
4149 int n_ann_on = xs_is_true(xs_dict_get_def(n_filter, "announces", n_def));
4150 int n_fol_on = xs_is_true(xs_dict_get_def(n_filter, "follows", n_def));
4151 int n_unfol_on = xs_is_true(xs_dict_get_def(n_filter, "unfollows", n_def));
4152 int n_folreq_on = xs_is_true(xs_dict_get_def(n_filter, "folreqs", n_def));
4153 int n_blocks_on = xs_is_true(xs_dict_get_def(n_filter, "blocks", n_def));
4154 int n_polls_on = xs_is_true(xs_dict_get_def(n_filter, "polls", n_def));
3977 4155
3978 xs_html *html = xs_html_tag("html", 4156 xs_html *html = xs_html_tag("html",
3979 html_user_head(user, NULL, NULL), 4157 html_user_head(user, NULL, NULL),
3980 body); 4158 body);
3981 4159
4160 xs *filter_notifs_action = xs_fmt("%s/admin/filter-notifications", user->actor);
4161 xs_html *notifs_form = xs_html_tag("form",
4162 xs_html_attr("autocomplete", "off"),
4163 xs_html_attr("method", "post"),
4164 xs_html_attr("action", filter_notifs_action),
4165 xs_html_attr("enctype", "multipart/form-data"),
4166 xs_html_attr("id", "filter"),
4167 xs_html_sctag("input",
4168 xs_html_attr("type", "hidden"),
4169 xs_html_attr("name", "hard-redir"),
4170 xs_html_attr("value", xs_fmt("%s/notifications", user->actor))),
4171 html_checkbox("likes_on", L("Likes"), n_likes_on),
4172 html_checkbox("reacts_on", L("Emoji reacts"), n_reacts_on),
4173 html_checkbox("mentions_on", L("Mentions"), n_ments_on),
4174 html_checkbox("announces_on", L("Announces"), n_ann_on),
4175 html_checkbox("follows_on", L("Follows"), n_fol_on),
4176 html_checkbox("unfollows_on", L("Unfollows"), n_unfol_on),
4177 html_checkbox("folreqs_on", L("Follow requests"), n_folreq_on),
4178 html_checkbox("blocks_on", L("Blocks"), n_blocks_on),
4179 html_checkbox("polls_on", L("Polls"), n_polls_on),
4180 xs_html_sctag("input",
4181 xs_html_attr("type", "submit"),
4182 xs_html_attr("class", "button"),
4183 xs_html_attr("value", L("Save"))));
4184
4185 xs_html_add(body,
4186 xs_html_tag("p",
4187 xs_html_tag("div",
4188 xs_html_attr("class", "snac-notify-filter"),
4189 xs_html_tag("details",
4190 xs_html_tag("summary",
4191 xs_html_text(L("Notifications filter..."))),
4192 notifs_form))));
4193
3982 xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor); 4194 xs *clear_all_action = xs_fmt("%s/admin/clear-notifications", user->actor);
3983 4195
3984 xs_html_add(body, 4196 xs_html_add(body,
@@ -4053,6 +4265,14 @@ xs_str *html_notifications(snac *user, int skip, int show)
4053 enqueue_actor_refresh(user, actor_id, 0); 4265 enqueue_actor_refresh(user, actor_id, 0);
4054 } 4266 }
4055 4267
4268 const char *html_url = xs_dict_get_def(actor, "url", actor_id);
4269
4270 if (xs_is_list(html_url))
4271 html_url = xs_list_get(html_url, 0);
4272
4273 if (!xs_is_string(html_url))
4274 html_url = actor_id;
4275
4056 xs *label_sanitized = sanitize(type); 4276 xs *label_sanitized = sanitize(type);
4057 const char *label = label_sanitized; 4277 const char *label = label_sanitized;
4058 4278
@@ -4078,6 +4298,8 @@ xs_str *html_notifications(snac *user, int skip, int show)
4078 isEmoji = 1; 4298 isEmoji = 1;
4079 4299
4080 if (xs_type(content) == XSTYPE_STRING) { 4300 if (xs_type(content) == XSTYPE_STRING) {
4301 if (!isEmoji && !n_likes_on)
4302 continue;
4081 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy); 4303 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
4082 wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji); 4304 wrk = xs_fmt("%s (%s&#xFE0F;)", isEmoji ? "EmojiReact" : "Like", emoji);
4083 label = wrk; 4305 label = wrk;
@@ -4087,14 +4309,14 @@ xs_str *html_notifications(snac *user, int skip, int show)
4087 if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id)) 4309 if (strcmp(type, "Follow") == 0 && pending_check(user, actor_id))
4088 label = L("Follow Request"); 4310 label = L("Follow Request");
4089 4311
4090 xs *s_date = xs_crop_i(xs_dup(date), 0, 10); 4312 xs *s_date = html_date_label(user, date);
4091 4313
4092 xs_html *this_html_label = xs_html_container( 4314 xs_html *this_html_label = xs_html_container(
4093 xs_html_tag("b", 4315 xs_html_tag("b",
4094 xs_html_raw(label), 4316 xs_html_raw(label),
4095 xs_html_text(" by "), 4317 xs_html_text(" by "),
4096 xs_html_tag("a", 4318 xs_html_tag("a",
4097 xs_html_attr("href", actor_id), 4319 xs_html_attr("href", html_url),
4098 xs_html_raw(a_name))), /* a_name is already sanitized */ 4320 xs_html_raw(a_name))), /* a_name is already sanitized */
4099 xs_html_text(" "), 4321 xs_html_text(" "),
4100 xs_html_tag("time", 4322 xs_html_tag("time",
@@ -4534,7 +4756,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
4534 4756
4535 xs *title = xs_fmt(L("Search results for account %s"), q); 4757 xs *title = xs_fmt(L("Search results for account %s"), q);
4536 4758
4537 page = html_people_list(&snac, l, title, "wf", NULL); 4759 page = html_people_list(&snac, l, title, "wf", NULL, 1);
4538 } 4760 }
4539 } 4761 }
4540 4762
@@ -5243,6 +5465,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
5243 if (valid_status(object_get(edit_id, &p_msg))) { 5465 if (valid_status(object_get(edit_id, &p_msg))) {
5244 /* copy relevant fields from previous version */ 5466 /* copy relevant fields from previous version */
5245 char *fields[] = { "id", "context", "url", 5467 char *fields[] = { "id", "context", "url",
5468 "cc", "attributedTo",
5246 "to", "inReplyTo", NULL }; 5469 "to", "inReplyTo", NULL };
5247 int n; 5470 int n;
5248 5471
@@ -5685,6 +5908,12 @@ int html_post_handler(const xs_dict *req, const char *q_path,
5685 status = HTTP_STATUS_SEE_OTHER; 5908 status = HTTP_STATUS_SEE_OTHER;
5686 } 5909 }
5687 else 5910 else
5911 if (p_path && strcmp(p_path, "admin/filter-notifications") == 0) { /** **/
5912 notify_filter(&snac, p_vars);
5913 user_persist(&snac, 0);
5914 status = HTTP_STATUS_SEE_OTHER;
5915 }
5916 else
5688 if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/ 5917 if (p_path && strcmp(p_path, "admin/clear-notifications") == 0) { /** **/
5689 notify_clear(&snac); 5918 notify_clear(&snac);
5690 timeline_touch(&snac); 5919 timeline_touch(&snac);
@@ -5813,6 +6042,33 @@ int html_post_handler(const xs_dict *req, const char *q_path,
5813 6042
5814 status = HTTP_STATUS_SEE_OTHER; 6043 status = HTTP_STATUS_SEE_OTHER;
5815 } 6044 }
6045 else
6046 if (p_path && strcmp(p_path, "admin/muted-words") == 0) {
6047 const char *words = xs_dict_get(p_vars, "muted_words");
6048
6049 if (xs_is_string(words)) {
6050 xs *new_words = xs_list_new();
6051 xs *l = xs_split(words, "\n");
6052 const char *v;
6053
6054 xs_list_foreach(l, v) {
6055 xs *s1 = xs_strip_i(xs_dup(v));
6056 s1 = xs_replace_i(s1, " ", "");
6057
6058 if (*s1 == '\0')
6059 continue;
6060
6061 xs *s2 = xs_utf8_to_lower(s1);
6062
6063 new_words = xs_list_insert_sorted(new_words, s2);
6064 }
6065
6066 snac.config = xs_dict_set(snac.config, "muted_words", new_words);
6067 user_persist(&snac, 0);
6068 }
6069
6070 status = HTTP_STATUS_SEE_OTHER;
6071 }
5816 6072
5817 if (status == HTTP_STATUS_SEE_OTHER) { 6073 if (status == HTTP_STATUS_SEE_OTHER) {
5818 const char *hard_redir = xs_dict_get(p_vars, "hard-redir"); 6074 const char *hard_redir = xs_dict_get(p_vars, "hard-redir");