summaryrefslogtreecommitdiff
path: root/html.c
diff options
context:
space:
mode:
Diffstat (limited to 'html.c')
-rw-r--r--html.c360
1 files changed, 289 insertions, 71 deletions
diff --git a/html.c b/html.c
index 78a9854..de2fdce 100644
--- a/html.c
+++ b/html.c
@@ -14,6 +14,7 @@
14#include "xs_curl.h" 14#include "xs_curl.h"
15#include "xs_unicode.h" 15#include "xs_unicode.h"
16#include "xs_url.h" 16#include "xs_url.h"
17#include "xs_random.h"
17 18
18#include "snac.h" 19#include "snac.h"
19 20
@@ -68,10 +69,14 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
68 } 69 }
69 70
70 xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems); 71 xs *style = xs_fmt("height: %dem; width: %dem; vertical-align: middle;", ems, ems);
72 xs *class = xs_fmt("snac-emoji snac-emoji-%d-em", ems);
71 73
72 const xs_dict *v; 74 const xs_dict *v;
73 int c = 0; 75 int c = 0;
74 76
77 xs_set rep_emoji;
78 xs_set_init(&rep_emoji);
79
75 while (xs_list_next(tag_list, &v, &c)) { 80 while (xs_list_next(tag_list, &v, &c)) {
76 const char *t = xs_dict_get(v, "type"); 81 const char *t = xs_dict_get(v, "type");
77 82
@@ -79,11 +84,21 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
79 const char *n = xs_dict_get(v, "name"); 84 const char *n = xs_dict_get(v, "name");
80 const xs_dict *i = xs_dict_get(v, "icon"); 85 const xs_dict *i = xs_dict_get(v, "icon");
81 86
87 /* avoid repeated emojis (Misskey seems to return this) */
88 if (xs_set_add(&rep_emoji, n) == 0)
89 continue;
90
82 if (xs_is_string(n) && xs_is_dict(i)) { 91 if (xs_is_string(n) && xs_is_dict(i)) {
83 const char *u = xs_dict_get(i, "url"); 92 const char *u = xs_dict_get(i, "url");
84 const char *mt = xs_dict_get(i, "mediaType"); 93 const char *mt = xs_dict_get(i, "mediaType");
85 94
86 if (xs_is_string(u) && xs_is_string(mt)) { 95 if (xs_is_string(u)) {
96 // on akkoma instances mediaType is not present.
97 // but we need to to know if the image is an svg or not.
98 // for now, i just use the file extention, which may not be the most reliable...
99 if (!xs_is_string(mt))
100 mt = xs_mime_by_ext(u);
101
87 if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) 102 if (strcmp(mt, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
88 s = xs_replace_i(s, n, ""); 103 s = xs_replace_i(s, n, "");
89 else { 104 else {
@@ -93,6 +108,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
93 xs_html_attr("loading", "lazy"), 108 xs_html_attr("loading", "lazy"),
94 xs_html_attr("src", url), 109 xs_html_attr("src", url),
95 xs_html_attr("alt", n), 110 xs_html_attr("alt", n),
111 xs_html_attr("title", n),
112 xs_html_attr("class", class),
96 xs_html_attr("style", style)); 113 xs_html_attr("style", style));
97 114
98 xs *s1 = xs_html_render(img); 115 xs *s1 = xs_html_render(img);
@@ -104,6 +121,8 @@ xs_str *replace_shortnames(xs_str *s, const xs_list *tag, int ems, const char *p
104 } 121 }
105 } 122 }
106 } 123 }
124
125 xs_set_free(&rep_emoji);
107 } 126 }
108 127
109 return s; 128 return s;
@@ -125,6 +144,26 @@ xs_str *actor_name(xs_dict *actor, const char *proxy)
125} 144}
126 145
127 146
147xs_str *format_text_with_emoji(snac *user, const char *text, int ems, const char *proxy)
148/* needed when we have local text with no tags attached */
149{
150 xs *tags = xs_list_new();
151 xs *name1 = not_really_markdown(text, NULL, &tags);
152
153 xs_str *name3;
154 if (user) {
155 xs *name2 = process_tags(user, name1, &tags);
156 name3 = sanitize(name2);
157 }
158 else {
159 name3 = sanitize(name1);
160 name3 = xs_replace_i(name3, "<br>", "");
161 }
162
163 return replace_shortnames(name3, tags, ems, proxy);
164}
165
166
128xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date, 167xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
129 const char *udate, const char *url, int priv, 168 const char *udate, const char *url, int priv,
130 int in_people, const char *proxy, const char *lang, 169 int in_people, const char *proxy, const char *lang,
@@ -177,7 +216,7 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
177 xs_html_attr("loading", "lazy"), 216 xs_html_attr("loading", "lazy"),
178 xs_html_attr("class", "snac-avatar"), 217 xs_html_attr("class", "snac-avatar"),
179 xs_html_attr("src", avatar), 218 xs_html_attr("src", avatar),
180 xs_html_attr("alt", "")), 219 xs_html_attr("alt", "[?]")),
181 xs_html_tag("a", 220 xs_html_tag("a",
182 xs_html_attr("href", href), 221 xs_html_attr("href", href),
183 xs_html_attr("class", "p-author h-card snac-author"), 222 xs_html_attr("class", "p-author h-card snac-author"),
@@ -293,7 +332,8 @@ xs_html *html_actor_icon(snac *user, xs_dict *actor, const char *date,
293} 332}
294 333
295 334
296xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, const char *proxy, const char *md5) 335xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg,
336 const char *proxy, const char *md5, const char *lang)
297{ 337{
298 xs *actor = NULL; 338 xs *actor = NULL;
299 xs_html *actor_icon = NULL; 339 xs_html *actor_icon = NULL;
@@ -302,7 +342,6 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con
302 const char *date = NULL; 342 const char *date = NULL;
303 const char *udate = NULL; 343 const char *udate = NULL;
304 const char *url = NULL; 344 const char *url = NULL;
305 const char *lang = NULL;
306 int priv = 0; 345 int priv = 0;
307 const char *type = xs_dict_get(msg, "type"); 346 const char *type = xs_dict_get(msg, "type");
308 347
@@ -314,16 +353,6 @@ xs_html *html_msg_icon(snac *user, const char *actor_id, const xs_dict *msg, con
314 date = xs_dict_get(msg, "published"); 353 date = xs_dict_get(msg, "published");
315 udate = xs_dict_get(msg, "updated"); 354 udate = xs_dict_get(msg, "updated");
316 355
317 lang = xs_dict_get(msg, "contentMap");
318 if (xs_is_dict(lang)) {
319 const char *v;
320 int c = 0;
321
322 xs_dict_next(lang, &lang, &v, &c);
323 }
324 else
325 lang = NULL;
326
327 actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang, md5); 356 actor_icon = html_actor_icon(user, actor, date, udate, url, priv, 0, proxy, lang, md5);
328 } 357 }
329 358
@@ -339,7 +368,7 @@ xs_html *html_note(snac *user, const char *summary,
339 const xs_val *mnt_only, const char *redir, 368 const xs_val *mnt_only, const char *redir,
340 const char *in_reply_to, int poll, 369 const char *in_reply_to, int poll,
341 const xs_list *att_files, const xs_list *att_alt_texts, 370 const xs_list *att_files, const xs_list *att_alt_texts,
342 int is_draft) 371 int is_draft, const char *published)
343/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */ 372/* Yes, this is a FUCKTON of arguments and I'm a bit embarrased */
344{ 373{
345 xs *action = xs_fmt("%s/admin/note", user->actor); 374 xs *action = xs_fmt("%s/admin/note", user->actor);
@@ -429,6 +458,42 @@ xs_html *html_note(snac *user, const char *summary,
429 xs_html_attr("name", "is_draft"), 458 xs_html_attr("name", "is_draft"),
430 xs_html_attr(is_draft ? "checked" : "", NULL)))); 459 xs_html_attr(is_draft ? "checked" : "", NULL))));
431 460
461 /* post date and time */
462 xs *post_date = NULL;
463 xs *post_time = NULL;
464
465 if (xs_is_string(published)) {
466 time_t t = xs_parse_iso_date(published, 0);
467
468 if (t > 0) {
469 post_date = xs_str_time(t, "%Y-%m-%d", 1);
470 post_time = xs_str_time(t, "%H:%M:%S", 1);
471 }
472 }
473
474 if (edit_id == NULL || is_draft || is_scheduled(user, edit_id)) {
475 xs *pdat = xs_fmt(L("Post date and time (timezone: %s):"), user->tz);
476
477 xs_html_add(form,
478 xs_html_tag("p",
479 xs_html_tag("details",
480 xs_html_tag("summary",
481 xs_html_text(L("Scheduled post..."))),
482 xs_html_tag("p",
483 xs_html_text(pdat),
484 xs_html_sctag("br", NULL),
485 xs_html_sctag("input",
486 xs_html_attr("type", "date"),
487 xs_html_attr("value", post_date ? post_date : ""),
488 xs_html_attr("name", "post_date")),
489 xs_html_text(" "),
490 xs_html_sctag("input",
491 xs_html_attr("type", "time"),
492 xs_html_attr("value", post_time ? post_time : ""),
493 xs_html_attr("step", "1"),
494 xs_html_attr("name", "post_time"))))));
495 }
496
432 if (edit_id) 497 if (edit_id)
433 xs_html_add(form, 498 xs_html_add(form,
434 xs_html_sctag("input", 499 xs_html_sctag("input",
@@ -746,11 +811,7 @@ xs_html *html_user_head(snac *user, const char *desc, const char *url)
746 811
747 /* show metrics in og:description? */ 812 /* show metrics in og:description? */
748 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) { 813 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
749 xs *fwers = follower_list(user); 814 xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
750 xs *fwing = following_list(user);
751
752 xs *s1 = xs_fmt(L("%d following, %d followers"),
753 xs_list_len(fwing), xs_list_len(fwers));
754 815
755 s1 = xs_str_cat(s1, " · "); 816 s1 = xs_str_cat(s1, " · ");
756 817
@@ -848,7 +909,9 @@ static xs_html *html_user_body(snac *user, int read_only)
848 } 909 }
849 else { 910 else {
850 int n_len = notify_new_num(user); 911 int n_len = notify_new_num(user);
912 int p_len = pending_count(user);
851 xs_html *notify_count = NULL; 913 xs_html *notify_count = NULL;
914 xs_html *pending_follow_count = NULL;
852 915
853 /* show the number of new notifications, if there are any */ 916 /* show the number of new notifications, if there are any */
854 if (n_len) { 917 if (n_len) {
@@ -860,6 +923,15 @@ static xs_html *html_user_body(snac *user, int read_only)
860 else 923 else
861 notify_count = xs_html_text(""); 924 notify_count = xs_html_text("");
862 925
926 if (p_len) {
927 xs *s = xs_fmt(" %d ", p_len);
928 pending_follow_count = xs_html_tag("sup",
929 xs_html_attr("style", "background-color: red; color: white;"),
930 xs_html_text(s));
931 }
932 else
933 pending_follow_count = xs_html_text("");
934
863 xs *admin_url = xs_fmt("%s/admin", user->actor); 935 xs *admin_url = xs_fmt("%s/admin", user->actor);
864 xs *notify_url = xs_fmt("%s/notifications", user->actor); 936 xs *notify_url = xs_fmt("%s/notifications", user->actor);
865 xs *people_url = xs_fmt("%s/people", user->actor); 937 xs *people_url = xs_fmt("%s/people", user->actor);
@@ -882,6 +954,7 @@ static xs_html *html_user_body(snac *user, int read_only)
882 xs_html_tag("a", 954 xs_html_tag("a",
883 xs_html_attr("href", people_url), 955 xs_html_attr("href", people_url),
884 xs_html_text(L("people"))), 956 xs_html_text(L("people"))),
957 pending_follow_count,
885 xs_html_text(" - "), 958 xs_html_text(" - "),
886 xs_html_tag("a", 959 xs_html_tag("a",
887 xs_html_attr("href", instance_url), 960 xs_html_attr("href", instance_url),
@@ -922,10 +995,12 @@ static xs_html *html_user_body(snac *user, int read_only)
922 xs_dict_get(user->config, "uid"), 995 xs_dict_get(user->config, "uid"),
923 xs_dict_get(srv_config, "host")); 996 xs_dict_get(srv_config, "host"));
924 997
998 xs *display_name = format_text_with_emoji(NULL, xs_dict_get(user->config, "name"), 1, proxy);
999
925 xs_html_add(top_user, 1000 xs_html_add(top_user,
926 xs_html_tag("p", 1001 xs_html_tag("p",
927 xs_html_attr("class", "p-name snac-top-user-name"), 1002 xs_html_attr("class", "p-name snac-top-user-name"),
928 xs_html_text(xs_dict_get(user->config, "name"))), 1003 xs_html_raw(display_name)),
929 xs_html_tag("p", 1004 xs_html_tag("p",
930 xs_html_attr("class", "snac-top-user-id"), 1005 xs_html_attr("class", "snac-top-user-id"),
931 xs_html_text(handle))); 1006 xs_html_text(handle)));
@@ -953,16 +1028,11 @@ static xs_html *html_user_body(snac *user, int read_only)
953 } 1028 }
954 1029
955 if (read_only) { 1030 if (read_only) {
956 xs *tags = xs_list_new(); 1031 xs *bio = format_text_with_emoji(user, xs_dict_get(user->config, "bio"), 2, proxy);
957 xs *bio1 = not_really_markdown(xs_dict_get(user->config, "bio"), NULL, &tags);
958 xs *bio2 = process_tags(user, bio1, &tags);
959 xs *bio3 = sanitize(bio2);
960
961 bio3 = replace_shortnames(bio3, tags, 2, proxy);
962 1032
963 xs_html *top_user_bio = xs_html_tag("div", 1033 xs_html *top_user_bio = xs_html_tag("div",
964 xs_html_attr("class", "p-note snac-top-user-bio"), 1034 xs_html_attr("class", "p-note snac-top-user-bio"),
965 xs_html_raw(bio3)); /* already sanitized */ 1035 xs_html_raw(bio)); /* already sanitized */
966 1036
967 xs_html_add(top_user, 1037 xs_html_add(top_user,
968 top_user_bio); 1038 top_user_bio);
@@ -1082,11 +1152,7 @@ static xs_html *html_user_body(snac *user, int read_only)
1082 } 1152 }
1083 1153
1084 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) { 1154 if (xs_is_true(xs_dict_get(user->config, "show_contact_metrics"))) {
1085 xs *fwers = follower_list(user); 1155 xs *s1 = xs_fmt(L("%d following, %d followers"), following_list_len(user), follower_list_len(user));
1086 xs *fwing = following_list(user);
1087
1088 xs *s1 = xs_fmt(L("%d following, %d followers"),
1089 xs_list_len(fwing), xs_list_len(fwers));
1090 1156
1091 xs_html_add(top_user, 1157 xs_html_add(top_user,
1092 xs_html_tag("p", 1158 xs_html_tag("p",
@@ -1116,7 +1182,7 @@ xs_html *html_top_controls(snac *user)
1116 NULL, NULL, 1182 NULL, NULL,
1117 xs_stock(XSTYPE_FALSE), "", 1183 xs_stock(XSTYPE_FALSE), "",
1118 xs_stock(XSTYPE_FALSE), NULL, 1184 xs_stock(XSTYPE_FALSE), NULL,
1119 NULL, 1, NULL, NULL, 0), 1185 NULL, 1, NULL, NULL, 0, NULL),
1120 1186
1121 /** operations **/ 1187 /** operations **/
1122 xs_html_tag("details", 1188 xs_html_tag("details",
@@ -1270,6 +1336,27 @@ xs_html *html_top_controls(snac *user)
1270 xs_html_attr("value", lang))); 1336 xs_html_attr("value", lang)));
1271 } 1337 }
1272 1338
1339 /* timezone */
1340 xs_html *tz_select = xs_html_tag("select",
1341 xs_html_attr("name", "tz"));
1342
1343 xs *tzs = xs_tz_list();
1344 const char *tz;
1345
1346 xs_list_foreach(tzs, tz) {
1347 if (strcmp(tz, user->tz) == 0)
1348 xs_html_add(tz_select,
1349 xs_html_tag("option",
1350 xs_html_text(tz),
1351 xs_html_attr("value", tz),
1352 xs_html_attr("selected", "selected")));
1353 else
1354 xs_html_add(tz_select,
1355 xs_html_tag("option",
1356 xs_html_text(tz),
1357 xs_html_attr("value", tz)));
1358 }
1359
1273 xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor); 1360 xs *user_setup_action = xs_fmt("%s/admin/user-setup", user->actor);
1274 1361
1275 xs_html_add(top_controls, 1362 xs_html_add(top_controls,
@@ -1466,6 +1553,11 @@ xs_html *html_top_controls(snac *user)
1466 lang_select), 1553 lang_select),
1467 1554
1468 xs_html_tag("p", 1555 xs_html_tag("p",
1556 xs_html_text(L("Time zone:")),
1557 xs_html_sctag("br", NULL),
1558 tz_select),
1559
1560 xs_html_tag("p",
1469 xs_html_text(L("New password:")), 1561 xs_html_text(L("New password:")),
1470 xs_html_sctag("br", NULL), 1562 xs_html_sctag("br", NULL),
1471 xs_html_sctag("input", 1563 xs_html_sctag("input",
@@ -1774,7 +1866,8 @@ xs_html *html_entry_controls(snac *user, const char *actor,
1774 id, NULL, 1866 id, NULL,
1775 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1867 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1776 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1868 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1777 NULL, 0, att_files, att_alt_texts, is_draft(user, id))), 1869 NULL, 0, att_files, att_alt_texts, is_draft(user, id),
1870 xs_dict_get(msg, "published"))),
1778 xs_html_tag("p", NULL)); 1871 xs_html_tag("p", NULL));
1779 } 1872 }
1780 1873
@@ -1793,7 +1886,7 @@ xs_html *html_entry_controls(snac *user, const char *actor,
1793 NULL, NULL, 1886 NULL, NULL,
1794 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"), 1887 xs_dict_get(msg, "sensitive"), xs_dict_get(msg, "summary"),
1795 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir, 1888 xs_stock(is_msg_public(msg) ? XSTYPE_FALSE : XSTYPE_TRUE), redir,
1796 id, 0, NULL, NULL, 0)), 1889 id, 0, NULL, NULL, 0, NULL)),
1797 xs_html_tag("p", NULL)); 1890 xs_html_tag("p", NULL));
1798 } 1891 }
1799 1892
@@ -1840,6 +1933,15 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1840 return xs_html_tag("mark", 1933 return xs_html_tag("mark",
1841 xs_html_text(L("Truncated (too deep)"))); 1934 xs_html_text(L("Truncated (too deep)")));
1842 1935
1936 const char *lang = NULL;
1937 const xs_dict *cmap = xs_dict_get(msg, "contentMap");
1938 if (xs_is_dict(cmap)) {
1939 const char *dummy;
1940 int c = 0;
1941
1942 xs_dict_next(cmap, &lang, &dummy, &c);
1943 }
1944
1843 if (strcmp(type, "Follow") == 0) { 1945 if (strcmp(type, "Follow") == 0) {
1844 return xs_html_tag("div", 1946 return xs_html_tag("div",
1845 xs_html_attr("class", "snac-post"), 1947 xs_html_attr("class", "snac-post"),
@@ -1848,7 +1950,7 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
1848 xs_html_tag("div", 1950 xs_html_tag("div",
1849 xs_html_attr("class", "snac-origin"), 1951 xs_html_attr("class", "snac-origin"),
1850 xs_html_text(L("follows you"))), 1952 xs_html_text(L("follows you"))),
1851 html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL))); 1953 html_msg_icon(read_only ? NULL : user, xs_dict_get(msg, "actor"), msg, proxy, NULL, lang)));
1852 } 1954 }
1853 else 1955 else
1854 if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) { 1956 if (!xs_match(type, POSTLIKE_OBJECT_TYPE)) {
@@ -2029,13 +2131,17 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2029 } 2131 }
2030 2132
2031 xs_html_add(post_header, 2133 xs_html_add(post_header,
2032 html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5)); 2134 html_msg_icon(read_only ? NULL : user, actor, msg, proxy, md5, lang));
2033 2135
2034 /** post content **/ 2136 /** post content **/
2035 2137
2036 xs_html *snac_content_wrap = xs_html_tag("div", 2138 xs_html *snac_content_wrap = xs_html_tag("div",
2037 xs_html_attr("class", "e-content snac-content")); 2139 xs_html_attr("class", "e-content snac-content"));
2038 2140
2141 if (xs_is_string(lang))
2142 xs_html_add(snac_content_wrap,
2143 xs_html_attr("lang", lang));
2144
2039 xs_html_add(entry, 2145 xs_html_add(entry,
2040 snac_content_wrap); 2146 snac_content_wrap);
2041 2147
@@ -2313,14 +2419,10 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2313 const char *o_href = xs_dict_get(a, "href"); 2419 const char *o_href = xs_dict_get(a, "href");
2314 const char *name = xs_dict_get(a, "name"); 2420 const char *name = xs_dict_get(a, "name");
2315 2421
2316 /* if this image is already in the post content, skip */ 2422 /* if this URL is already in the post content, skip */
2317 if (content && xs_str_in(content, o_href) != -1) 2423 if (content && xs_str_in(content, o_href) != -1)
2318 continue; 2424 continue;
2319 2425
2320 /* drop silently any attachment that may include JavaScript */
2321 if (strcmp(type, "text/html") == 0)
2322 continue;
2323
2324 if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg"))) 2426 if (strcmp(type, "image/svg+xml") == 0 && !xs_is_true(xs_dict_get(srv_config, "enable_svg")))
2325 continue; 2427 continue;
2326 2428
@@ -2396,13 +2498,19 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2396 name = NULL; 2498 name = NULL;
2397 } 2499 }
2398 else { 2500 else {
2501 xs *d_href = xs_dup(o_href);
2502 if (strlen(d_href) > 64) {
2503 d_href[64] = '\0';
2504 d_href = xs_str_cat(d_href, "...");
2505 }
2506
2399 xs_html_add(content_attachments, 2507 xs_html_add(content_attachments,
2400 xs_html_tag("p", 2508 xs_html_tag("p",
2401 xs_html_tag("a", 2509 xs_html_tag("a",
2402 xs_html_attr("href", o_href), 2510 xs_html_attr("href", o_href),
2403 xs_html_text(L("Attachment")), 2511 xs_html_text(L("Attachment")),
2404 xs_html_text(": "), 2512 xs_html_text(": "),
2405 xs_html_text(o_href)))); 2513 xs_html_text(d_href))));
2406 2514
2407 /* do not generate an Alt... */ 2515 /* do not generate an Alt... */
2408 name = NULL; 2516 name = NULL;
@@ -2689,6 +2797,11 @@ xs_html *html_entry(snac *user, xs_dict *msg, int read_only,
2689 } 2797 }
2690 } 2798 }
2691 2799
2800 /* add an invisible hr, to help differentiate between posts in text browsers */
2801 xs_html_add(entry_top,
2802 xs_html_sctag("hr",
2803 xs_html_attr("hidden", NULL)));
2804
2692 return entry_top; 2805 return entry_top;
2693} 2806}
2694 2807
@@ -2830,6 +2943,18 @@ xs_str *html_timeline(snac *user, const xs_list *list, int read_only,
2830 xs_html_text(L("drafts"))))); 2943 xs_html_text(L("drafts")))));
2831 } 2944 }
2832 2945
2946 {
2947 /* show the list of scheduled posts */
2948 xs *url = xs_fmt("%s/sched", user->actor);
2949 xs_html_add(lol,
2950 xs_html_tag("li",
2951 xs_html_tag("a",
2952 xs_html_attr("href", url),
2953 xs_html_attr("class", "snac-list-link"),
2954 xs_html_attr("title", L("Scheduled posts")),
2955 xs_html_text(L("scheduled posts")))));
2956 }
2957
2833 /* the list of followed hashtags */ 2958 /* the list of followed hashtags */
2834 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags"); 2959 const char *followed_hashtags = xs_dict_get(user->config, "followed_hashtags");
2835 2960
@@ -3019,6 +3144,8 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
3019 xs_html_tag("summary", 3144 xs_html_tag("summary",
3020 xs_html_text("...")))); 3145 xs_html_text("..."))));
3021 3146
3147 xs *redir = xs_fmt("%s/people", user->actor);
3148
3022 const char *actor_id; 3149 const char *actor_id;
3023 3150
3024 xs_list_foreach(list, actor_id) { 3151 xs_list_foreach(list, actor_id) {
@@ -3040,6 +3167,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
3040 3167
3041 if (!xs_is_null(c)) { 3168 if (!xs_is_null(c)) {
3042 xs *sc = sanitize(c); 3169 xs *sc = sanitize(c);
3170 sc = replace_shortnames(sc, xs_dict_get(actor, "tag"), 2, proxy);
3043 3171
3044 xs_html *snac_content = xs_html_tag("div", 3172 xs_html *snac_content = xs_html_tag("div",
3045 xs_html_attr("class", "snac-content")); 3173 xs_html_attr("class", "snac-content"));
@@ -3071,6 +3199,10 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
3071 xs_html_attr("value", actor_id)), 3199 xs_html_attr("value", actor_id)),
3072 xs_html_sctag("input", 3200 xs_html_sctag("input",
3073 xs_html_attr("type", "hidden"), 3201 xs_html_attr("type", "hidden"),
3202 xs_html_attr("name", "hard-redir"),
3203 xs_html_attr("value", redir)),
3204 xs_html_sctag("input",
3205 xs_html_attr("type", "hidden"),
3074 xs_html_attr("name", "actor-form"), 3206 xs_html_attr("name", "actor-form"),
3075 xs_html_attr("value", "yes"))); 3207 xs_html_attr("value", "yes")));
3076 3208
@@ -3130,7 +3262,7 @@ xs_html *html_people_list(snac *user, xs_list *list, const char *header, const c
3130 NULL, actor_id, 3262 NULL, actor_id,
3131 xs_stock(XSTYPE_FALSE), "", 3263 xs_stock(XSTYPE_FALSE), "",
3132 xs_stock(XSTYPE_FALSE), NULL, 3264 xs_stock(XSTYPE_FALSE), NULL,
3133 NULL, 0, NULL, NULL, 0), 3265 NULL, 0, NULL, NULL, 0, NULL),
3134 xs_html_tag("p", NULL)); 3266 xs_html_tag("p", NULL));
3135 3267
3136 xs_html_add(snac_post, snac_controls); 3268 xs_html_add(snac_post, snac_controls);
@@ -3257,7 +3389,8 @@ xs_str *html_notifications(snac *user, int skip, int show)
3257 continue; 3389 continue;
3258 3390
3259 xs *a_name = actor_name(actor, proxy); 3391 xs *a_name = actor_name(actor, proxy);
3260 const char *label = type; 3392 xs *label_sanatized = sanitize(type);
3393 const char *label = label_sanatized;
3261 3394
3262 if (strcmp(type, "Create") == 0) 3395 if (strcmp(type, "Create") == 0)
3263 label = L("Mention"); 3396 label = L("Mention");
@@ -3268,11 +3401,12 @@ xs_str *html_notifications(snac *user, int skip, int show)
3268 if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0) 3401 if (strcmp(type, "Undo") == 0 && strcmp(utype, "Follow") == 0)
3269 label = L("Unfollow"); 3402 label = L("Unfollow");
3270 else 3403 else
3271 if (strcmp(type, "EmojiReact") == 0) { 3404 if (strcmp(type, "EmojiReact") == 0 || strcmp(type, "Like") == 0) {
3272 const char *content = xs_dict_get_path(noti, "msg.content"); 3405 const char *content = xs_dict_get_path(noti, "msg.content");
3273 3406
3274 if (xs_type(content) == XSTYPE_STRING) { 3407 if (xs_type(content) == XSTYPE_STRING) {
3275 wrk = xs_fmt("%s (%s)", type, content); 3408 xs *emoji = replace_shortnames(xs_dup(content), xs_dict_get_path(noti, "msg.tag"), 1, proxy);
3409 wrk = xs_fmt("%s (%s&#xFE0F;)", type, emoji);
3276 label = wrk; 3410 label = wrk;
3277 } 3411 }
3278 } 3412 }
@@ -3284,7 +3418,7 @@ xs_str *html_notifications(snac *user, int skip, int show)
3284 3418
3285 xs_html *this_html_label = xs_html_container( 3419 xs_html *this_html_label = xs_html_container(
3286 xs_html_tag("b", 3420 xs_html_tag("b",
3287 xs_html_text(label), 3421 xs_html_raw(label),
3288 xs_html_text(" by "), 3422 xs_html_text(" by "),
3289 xs_html_tag("a", 3423 xs_html_tag("a",
3290 xs_html_attr("href", actor_id), 3424 xs_html_attr("href", actor_id),
@@ -3584,7 +3718,7 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3584 3718
3585 if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines"))) 3719 if (xs_is_true(xs_dict_get(srv_config, "strict_public_timelines")))
3586 list = timeline_simple_list(&snac, "public", skip, show, &more); 3720 list = timeline_simple_list(&snac, "public", skip, show, &more);
3587 else 3721 else
3588 list = timeline_list(&snac, "public", skip, show, &more); 3722 list = timeline_list(&snac, "public", skip, show, &more);
3589 3723
3590 xs *pins = pinned_list(&snac); 3724 xs *pins = pinned_list(&snac);
@@ -3879,6 +4013,21 @@ int html_get_handler(const xs_dict *req, const char *q_path,
3879 } 4013 }
3880 } 4014 }
3881 else 4015 else
4016 if (strcmp(p_path, "sched") == 0) { /** list of scheduled posts **/
4017 if (!login(&snac, req)) {
4018 *body = xs_dup(uid);
4019 status = HTTP_STATUS_UNAUTHORIZED;
4020 }
4021 else {
4022 xs *list = scheduled_list(&snac);
4023
4024 *body = html_timeline(&snac, list, 0, skip, show,
4025 0, L("Scheduled posts"), "", 0, error);
4026 *b_size = strlen(*body);
4027 status = HTTP_STATUS_OK;
4028 }
4029 }
4030 else
3882 if (xs_startswith(p_path, "list/")) { /** list timelines **/ 4031 if (xs_startswith(p_path, "list/")) { /** list timelines **/
3883 if (!login(&snac, req)) { 4032 if (!login(&snac, req)) {
3884 *body = xs_dup(uid); 4033 *body = xs_dup(uid);
@@ -4175,12 +4324,14 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4175 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path)); 4324 snac_debug(&snac, 1, xs_fmt("web action '%s' received", p_path));
4176 4325
4177 /* post note */ 4326 /* post note */
4178 const xs_str *content = xs_dict_get(p_vars, "content"); 4327 const char *content = xs_dict_get(p_vars, "content");
4179 const xs_str *in_reply_to = xs_dict_get(p_vars, "in_reply_to"); 4328 const char *in_reply_to = xs_dict_get(p_vars, "in_reply_to");
4180 const xs_str *to = xs_dict_get(p_vars, "to"); 4329 const char *to = xs_dict_get(p_vars, "to");
4181 const xs_str *sensitive = xs_dict_get(p_vars, "sensitive"); 4330 const char *sensitive = xs_dict_get(p_vars, "sensitive");
4182 const xs_str *summary = xs_dict_get(p_vars, "summary"); 4331 const char *summary = xs_dict_get(p_vars, "summary");
4183 const xs_str *edit_id = xs_dict_get(p_vars, "edit_id"); 4332 const char *edit_id = xs_dict_get(p_vars, "edit_id");
4333 const char *post_date = xs_dict_get_def(p_vars, "post_date", "");
4334 const char *post_time = xs_dict_get_def(p_vars, "post_time", "");
4184 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only")); 4335 int priv = !xs_is_null(xs_dict_get(p_vars, "mentioned_only"));
4185 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft")); 4336 int store_as_draft = !xs_is_null(xs_dict_get(p_vars, "is_draft"));
4186 xs *attach_list = xs_list_new(); 4337 xs *attach_list = xs_list_new();
@@ -4210,9 +4361,12 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4210 const char *fn = xs_list_get(attach_file, 0); 4361 const char *fn = xs_list_get(attach_file, 0);
4211 4362
4212 if (xs_is_string(fn) && *fn != '\0') { 4363 if (xs_is_string(fn) && *fn != '\0') {
4213 char *ext = strrchr(fn, '.'); 4364 char rnd[32];
4214 xs *hash = xs_md5_hex(fn, strlen(fn)); 4365 xs_rnd_buf(rnd, sizeof(rnd));
4215 xs *id = xs_fmt("%s%s", hash, ext); 4366
4367 const char *ext = strrchr(fn, '.');
4368 xs *hash = xs_md5_hex(rnd, strlen(rnd));
4369 xs *id = xs_fmt("post-%s%s", hash, ext ? ext : "");
4216 xs *url = xs_fmt("%s/s/%s", snac.actor, id); 4370 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
4217 int fo = xs_number_get(xs_list_get(attach_file, 1)); 4371 int fo = xs_number_get(xs_list_get(attach_file, 1));
4218 int fs = xs_number_get(xs_list_get(attach_file, 2)); 4372 int fs = xs_number_get(xs_list_get(attach_file, 2));
@@ -4268,6 +4422,31 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4268 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary); 4422 msg = xs_dict_set(msg, "summary", xs_is_null(summary) ? "..." : summary);
4269 } 4423 }
4270 4424
4425 if (xs_is_string(post_date) && *post_date) {
4426 xs *post_pubdate = xs_fmt("%sT%s", post_date,
4427 xs_is_string(post_time) && *post_time ? post_time : "00:00:00");
4428
4429 time_t t = xs_parse_iso_date(post_pubdate, 0);
4430
4431 if (t != 0) {
4432 t -= xs_tz_offset(snac.tz);
4433
4434 xs *iso_date = xs_str_iso_date(t);
4435 msg = xs_dict_set(msg, "published", iso_date);
4436
4437 snac_debug(&snac, 1, xs_fmt("Published date: [%s]", iso_date));
4438 }
4439 else
4440 snac_log(&snac, xs_fmt("Invalid post date: [%s]", post_pubdate));
4441 }
4442
4443 /* is the published date from the future? */
4444 int future_post = 0;
4445 xs *right_now = xs_str_utctime(0, ISO_DATE_SPEC);
4446
4447 if (strcmp(xs_dict_get(msg, "published"), right_now) > 0)
4448 future_post = 1;
4449
4271 if (xs_is_null(edit_id)) { 4450 if (xs_is_null(edit_id)) {
4272 /* new message */ 4451 /* new message */
4273 const char *id = xs_dict_get(msg, "id"); 4452 const char *id = xs_dict_get(msg, "id");
@@ -4275,6 +4454,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4275 if (store_as_draft) { 4454 if (store_as_draft) {
4276 draft_add(&snac, id, msg); 4455 draft_add(&snac, id, msg);
4277 } 4456 }
4457 else
4458 if (future_post) {
4459 schedule_add(&snac, id, msg);
4460 }
4278 else { 4461 else {
4279 c_msg = msg_create(&snac, msg); 4462 c_msg = msg_create(&snac, msg);
4280 timeline_add(&snac, id, msg); 4463 timeline_add(&snac, id, msg);
@@ -4286,7 +4469,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4286 4469
4287 if (valid_status(object_get(edit_id, &p_msg))) { 4470 if (valid_status(object_get(edit_id, &p_msg))) {
4288 /* copy relevant fields from previous version */ 4471 /* copy relevant fields from previous version */
4289 char *fields[] = { "id", "context", "url", "published", 4472 char *fields[] = { "id", "context", "url",
4290 "to", "inReplyTo", NULL }; 4473 "to", "inReplyTo", NULL };
4291 int n; 4474 int n;
4292 4475
@@ -4302,18 +4485,34 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4302 if (is_draft(&snac, edit_id)) { 4485 if (is_draft(&snac, edit_id)) {
4303 /* message was previously a draft; it's a create activity */ 4486 /* message was previously a draft; it's a create activity */
4304 4487
4305 /* set the published field to now */ 4488 /* if the date is from the past, overwrite it with right_now */
4306 xs *published = xs_str_utctime(0, ISO_DATE_SPEC); 4489 if (strcmp(xs_dict_get(msg, "published"), right_now) < 0) {
4307 msg = xs_dict_set(msg, "published", published); 4490 snac_debug(&snac, 1, xs_fmt("setting draft ancient date to %s", right_now));
4491 msg = xs_dict_set(msg, "published", right_now);
4492 }
4308 4493
4309 /* overwrite object */ 4494 /* overwrite object */
4310 object_add_ow(edit_id, msg); 4495 object_add_ow(edit_id, msg);
4311 4496
4312 c_msg = msg_create(&snac, msg); 4497 if (future_post) {
4313 timeline_add(&snac, edit_id, msg); 4498 schedule_add(&snac, edit_id, msg);
4499 }
4500 else {
4501 c_msg = msg_create(&snac, msg);
4502 timeline_add(&snac, edit_id, msg);
4503 }
4504
4314 draft_del(&snac, edit_id); 4505 draft_del(&snac, edit_id);
4315 } 4506 }
4507 else
4508 if (is_scheduled(&snac, edit_id)) {
4509 /* editing an scheduled post; just update it */
4510 schedule_add(&snac, edit_id, msg);
4511 }
4316 else { 4512 else {
4513 /* ignore the (possibly changed) published date */
4514 msg = xs_dict_set(msg, "published", xs_dict_get(p_msg, "published"));
4515
4317 /* set the updated field */ 4516 /* set the updated field */
4318 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC); 4517 xs *updated = xs_str_utctime(0, ISO_DATE_SPEC);
4319 msg = xs_dict_set(msg, "updated", updated); 4518 msg = xs_dict_set(msg, "updated", updated);
@@ -4329,8 +4528,10 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4329 snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id)); 4528 snac_log(&snac, xs_fmt("cannot get object '%s' for editing", edit_id));
4330 } 4529 }
4331 4530
4332 if (c_msg != NULL) 4531 if (c_msg != NULL) {
4333 enqueue_message(&snac, c_msg); 4532 enqueue_message(&snac, c_msg);
4533 enqueue_webmention(msg);
4534 }
4334 4535
4335 history_del(&snac, "timeline.html_"); 4536 history_del(&snac, "timeline.html_");
4336 } 4537 }
@@ -4398,6 +4599,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4398 if (is_draft(&snac, id)) 4599 if (is_draft(&snac, id))
4399 draft_del(&snac, id); 4600 draft_del(&snac, id);
4400 else 4601 else
4602 if (is_scheduled(&snac, id))
4603 schedule_del(&snac, id);
4604 else
4401 hide(&snac, id); 4605 hide(&snac, id);
4402 } 4606 }
4403 else 4607 else
@@ -4493,6 +4697,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4493 4697
4494 draft_del(&snac, id); 4698 draft_del(&snac, id);
4495 4699
4700 schedule_del(&snac, id);
4701
4496 snac_log(&snac, xs_fmt("deleted entry %s", id)); 4702 snac_log(&snac, xs_fmt("deleted entry %s", id));
4497 } 4703 }
4498 } 4704 }
@@ -4613,6 +4819,8 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4613 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE)); 4819 snac.config = xs_dict_set(snac.config, "show_contact_metrics", xs_stock(XSTYPE_FALSE));
4614 if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL) 4820 if ((v = xs_dict_get(p_vars, "web_ui_lang")) != NULL)
4615 snac.config = xs_dict_set(snac.config, "lang", v); 4821 snac.config = xs_dict_set(snac.config, "lang", v);
4822 if ((v = xs_dict_get(p_vars, "tz")) != NULL)
4823 snac.config = xs_dict_set(snac.config, "tz", v);
4616 4824
4617 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", "")); 4825 snac.config = xs_dict_set(snac.config, "latitude", xs_dict_get_def(p_vars, "latitude", ""));
4618 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", "")); 4826 snac.config = xs_dict_set(snac.config, "longitude", xs_dict_get_def(p_vars, "longitude", ""));
@@ -4636,7 +4844,7 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4636 if (xs_startswith(mimetype, "image/")) { 4844 if (xs_startswith(mimetype, "image/")) {
4637 const char *ext = strrchr(fn, '.'); 4845 const char *ext = strrchr(fn, '.');
4638 xs *hash = xs_md5_hex(fn, strlen(fn)); 4846 xs *hash = xs_md5_hex(fn, strlen(fn));
4639 xs *id = xs_fmt("%s%s", hash, ext); 4847 xs *id = xs_fmt("%s-%s%s", uploads[n], hash, ext ? ext : "");
4640 xs *url = xs_fmt("%s/s/%s", snac.actor, id); 4848 xs *url = xs_fmt("%s/s/%s", snac.actor, id);
4641 int fo = xs_number_get(xs_list_get(uploaded_file, 1)); 4849 int fo = xs_number_get(xs_list_get(uploaded_file, 1));
4642 int fs = xs_number_get(xs_list_get(uploaded_file, 2)); 4850 int fs = xs_number_get(xs_list_get(uploaded_file, 2));
@@ -4705,6 +4913,9 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4705 /* set the option */ 4913 /* set the option */
4706 msg = xs_dict_append(msg, "name", v); 4914 msg = xs_dict_append(msg, "name", v);
4707 4915
4916 /* delete the content */
4917 msg = xs_dict_del(msg, "content");
4918
4708 xs *c_msg = msg_create(&snac, msg); 4919 xs *c_msg = msg_create(&snac, msg);
4709 4920
4710 enqueue_message(&snac, c_msg); 4921 enqueue_message(&snac, c_msg);
@@ -4793,12 +5004,19 @@ int html_post_handler(const xs_dict *req, const char *q_path,
4793 } 5004 }
4794 5005
4795 if (status == HTTP_STATUS_SEE_OTHER) { 5006 if (status == HTTP_STATUS_SEE_OTHER) {
4796 const char *redir = xs_dict_get(p_vars, "redir"); 5007 const char *hard_redir = xs_dict_get(p_vars, "hard-redir");
4797 5008
4798 if (xs_is_null(redir)) 5009 if (xs_is_string(hard_redir))
4799 redir = "top"; 5010 *body = xs_dup(hard_redir);
5011 else {
5012 const char *redir = xs_dict_get(p_vars, "redir");
5013
5014 if (xs_is_null(redir))
5015 redir = "top";
5016
5017 *body = xs_fmt("%s/admin#%s", snac.actor, redir);
5018 }
4800 5019
4801 *body = xs_fmt("%s/admin#%s", snac.actor, redir);
4802 *b_size = strlen(*body); 5020 *b_size = strlen(*body);
4803 } 5021 }
4804 5022