| | @@ -378,11 +378,11 @@ |
| 378 | 378 | int trid; /* RID of new control artifact */ |
| 379 | 379 | char *zUuid; /* UUID of head version of post */ |
| 380 | 380 | |
| 381 | 381 | db_begin_transaction(); |
| 382 | 382 | frid = forumpost_head_rid(frid); |
| 383 | | - iTagged = forum_rid_is_tagged(frid, "closed", 1); |
| 383 | + iTagged = forum_rid_is_tagged(frid, zTagName, 1); |
| 384 | 384 | if( (iTagged && addTag |
| 385 | 385 | /* Already tagged, noting that in the case of (addTag<0) it may |
| 386 | 386 | ** actually be a parent which is tagged. */) |
| 387 | 387 | || (iTagged<=0 && !addTag |
| 388 | 388 | /* This entry is not tagged, but a parent post may be. */) ){ |
| | @@ -925,21 +925,25 @@ |
| 925 | 925 | static void forum_render_attachment_list2(ForumPost *p){ |
| 926 | 926 | if( p->pEditHead ) p = p->pEditHead; |
| 927 | 927 | forum_render_attachment_list(p->zUuid); |
| 928 | 928 | } |
| 929 | 929 | |
| 930 | +/* Flags for use with forum_display_post() */ |
| 931 | +#define FDISPLAY_RAW 0x01 /* omit the border */ |
| 932 | +#define FDISPLAY_UNFORMATTED 0x02 /* leave the post unformatted */ |
| 933 | +#define FDISPLAY_HISTORY 0x04 /* Showing edit history */ |
| 934 | +#define FDISPLAY_SELECTED 0x08 /* This is the selected post */ |
| 935 | +#define FDISPLAY_ISROOT 0x10 /* This is the root post */ |
| 936 | + |
| 930 | 937 | /* |
| 931 | 938 | ** Display a single post in a forum thread. |
| 932 | 939 | */ |
| 933 | 940 | static void forum_display_post( |
| 934 | 941 | ForumThread *pThread, /* The thread that this post is a member of */ |
| 935 | 942 | ForumPost *p, /* Forum post to display */ |
| 936 | 943 | int iIndentScale, /* Indent scale factor */ |
| 937 | | - int bRaw, /* True to omit the border */ |
| 938 | | - int bUnf, /* True to leave the post unformatted */ |
| 939 | | - int bHist, /* True if showing edit history */ |
| 940 | | - int bSelect, /* True if this is the selected post */ |
| 944 | + int flags, /* From the FDISPLAY_... enum */ |
| 941 | 945 | char *zQuery /* Common query string */ |
| 942 | 946 | ){ |
| 943 | 947 | char *zPosterName; /* Name of user who originally made this post */ |
| 944 | 948 | char *zEditorName; /* Name of user who provided the current edit */ |
| 945 | 949 | char *zDate; /* The time/date string */ |
| | @@ -948,10 +952,14 @@ |
| 948 | 952 | Manifest *pManifest; /* Manifest comprising the current post */ |
| 949 | 953 | int bPrivate; /* True for posts awaiting moderation */ |
| 950 | 954 | int bSameUser; /* True if author is also the reader */ |
| 951 | 955 | int iIndent; /* Indent level */ |
| 952 | 956 | int iClosed; /* True if (sub)thread is closed */ |
| 957 | + const int bRaw = flags & FDISPLAY_RAW; |
| 958 | + const int bUnf = flags & FDISPLAY_UNFORMATTED; |
| 959 | + const int bHist = flags & FDISPLAY_HISTORY; |
| 960 | + const int bSelect = flags & FDISPLAY_SELECTED; |
| 953 | 961 | const char *zMimetype;/* Formatting MIME type */ |
| 954 | 962 | |
| 955 | 963 | /* Get the manifest for the post. Abort if not found (e.g. shunned). */ |
| 956 | 964 | pManifest = manifest_get(p->fpid, CFTYPE_FORUM, 0); |
| 957 | 965 | if( !pManifest ) return; |
| | @@ -1120,14 +1128,15 @@ |
| 1120 | 1128 | const ForumPost *pHead = p->pEditHead ? p->pEditHead : p; |
| 1121 | 1129 | if( forumpost_may_close() && iClosed>=0 ){ |
| 1122 | 1130 | @ <form method="post" \ |
| 1123 | 1131 | @ action='%R/forumpost_%s(iClosed > 0 ? "reopen" : "close")'> |
| 1124 | 1132 | login_insert_csrf_secret(); |
| 1125 | | - @ <input type="hidden" name="fpid" value="%s(pHead->zUuid)" /> |
| 1133 | + @ <input type="hidden" name="fpid" value="%s(p->zUuid)" /> |
| 1126 | 1134 | if( moderation_pending(p->fpid)==0 ){ |
| 1127 | 1135 | @ <input type="button" value='%s(iClosed ? "Re-open" : "Close")' \ |
| 1128 | | - @ class='%s(iClosed ? "action-reopen" : "action-close")'/> |
| 1136 | + @ class='hidden %s(iClosed ? "action-reopen" : "action-close")'/> |
| 1137 | + /* ^^^ activated by fossil.page.forumpost.js */ |
| 1129 | 1138 | } |
| 1130 | 1139 | @ </form> |
| 1131 | 1140 | } |
| 1132 | 1141 | if( g.perm.Admin || |
| 1133 | 1142 | (login_is_individual() |
| | @@ -1140,10 +1149,21 @@ |
| 1140 | 1149 | @ <input type="hidden" name="forumpost" value="%T(pHead->zUuid)"> |
| 1141 | 1150 | @ <input type="submit" value="Attach..."> |
| 1142 | 1151 | login_insert_csrf_secret(); |
| 1143 | 1152 | moderation_pending_www(p->fpid); |
| 1144 | 1153 | @ </form> |
| 1154 | + } |
| 1155 | + if( !p->pIrt && g.perm.Setup ){ |
| 1156 | + const int isPinned = forum_rid_is_tagged(pHead->fpid, "pinned", 0); |
| 1157 | + @ <form method="post" \ |
| 1158 | + @ action='%R/forumpost_%s(isPinned ? "unpin" : "pin")'> |
| 1159 | + login_insert_csrf_secret(); |
| 1160 | + @ <input type="hidden" name="fpid" value="%s(p->zUuid)" /> |
| 1161 | + @ <input type="button" value='%s(isPinned ? "Unpin" : "Pin")' \ |
| 1162 | + @ class='hidden %s(isPinned ? "action-unpin" : "action-pin")'/> |
| 1163 | + /* ^^^ activated by fossil.page.forumpost.js */ |
| 1164 | + @ </form> |
| 1145 | 1165 | } |
| 1146 | 1166 | } |
| 1147 | 1167 | @ </div> |
| 1148 | 1168 | } |
| 1149 | 1169 | @ </div> |
| | @@ -1259,12 +1279,19 @@ |
| 1259 | 1279 | } |
| 1260 | 1280 | |
| 1261 | 1281 | /* Display the appropriate subset of posts in sequence. */ |
| 1262 | 1282 | while( p ){ |
| 1263 | 1283 | /* Display the post. */ |
| 1264 | | - forum_display_post(pThread, p, iIndentScale, mode==FD_RAW, |
| 1265 | | - bUnf, bHist, p==pSelect, zQuery); |
| 1284 | + forum_display_post( |
| 1285 | + pThread, p, iIndentScale, |
| 1286 | + (mode==FD_RAW ? FDISPLAY_RAW : 0) | |
| 1287 | + (bUnf ? FDISPLAY_UNFORMATTED : 0) | |
| 1288 | + (bHist ? FDISPLAY_HISTORY : 0) | |
| 1289 | + (p==pSelect ? FDISPLAY_SELECTED : 0) | |
| 1290 | + ((0==fpid || fpid==froot) ? FDISPLAY_ISROOT : 0), |
| 1291 | + zQuery |
| 1292 | + ); |
| 1266 | 1293 | |
| 1267 | 1294 | /* Advance to the next post in the thread. */ |
| 1268 | 1295 | if( mode==FD_CHRONO ){ |
| 1269 | 1296 | /* Chronological mode: display posts (optionally including edits) in their |
| 1270 | 1297 | ** original commit order. */ |
| | @@ -1634,10 +1661,29 @@ |
| 1634 | 1661 | mimetype_option_menu(zMimetype, "mimetype"); |
| 1635 | 1662 | @ <div class="forum-editor-widget"> |
| 1636 | 1663 | @ <textarea aria-label="Content:" name="content" class="wikiedit" \ |
| 1637 | 1664 | @ cols="80" rows="25" wrap="virtual">%h(zContent)</textarea></div> |
| 1638 | 1665 | } |
| 1666 | + |
| 1667 | +/* |
| 1668 | +** Internal helper for /forumpost_XYZ internal pages which tag/untag |
| 1669 | +** posts. |
| 1670 | +*/ |
| 1671 | +static void forumpost_action_helper(const char *zTag, const char *zVal, |
| 1672 | + int addTag){ |
| 1673 | + const char *zFpid = PD("fpid",""); |
| 1674 | + int fpid; |
| 1675 | + |
| 1676 | + cgi_csrf_verify(); |
| 1677 | + fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1678 | + if( fpid<=0 ){ |
| 1679 | + webpage_error("Missing or invalid fpid query parameter"); |
| 1680 | + } |
| 1681 | + forumpost_tag(fpid, zTag, addTag, zVal); |
| 1682 | + cgi_redirectf("%R/forumpost/%S",zFpid); |
| 1683 | + return; |
| 1684 | +} |
| 1639 | 1685 | |
| 1640 | 1686 | /* |
| 1641 | 1687 | ** WEBPAGE: forumpost_close hidden |
| 1642 | 1688 | ** WEBPAGE: forumpost_reopen hidden |
| 1643 | 1689 | ** |
| | @@ -1648,30 +1694,39 @@ |
| 1648 | 1694 | ** API for forumpost_tag(). After (perhaps) modifying the "closed" |
| 1649 | 1695 | ** status of the given thread, it redirects to that post's thread |
| 1650 | 1696 | ** view. Requires admin privileges. |
| 1651 | 1697 | */ |
| 1652 | 1698 | void forum_page_close(void){ |
| 1653 | | - const char *zFpid = PD("fpid",""); |
| 1654 | | - const char *zReason = 0; |
| 1655 | | - int fClose; |
| 1656 | | - int fpid; |
| 1657 | | - |
| 1658 | 1699 | login_check_credentials(); |
| 1659 | 1700 | if( forumpost_may_close()==0 ){ |
| 1660 | 1701 | login_needed(g.anon.Admin); |
| 1661 | | - return; |
| 1662 | | - } |
| 1663 | | - cgi_csrf_verify(); |
| 1664 | | - fpid = symbolic_name_to_rid(zFpid, "f"); |
| 1665 | | - if( fpid<=0 ){ |
| 1666 | | - webpage_error("Missing or invalid fpid query parameter"); |
| 1667 | | - } |
| 1668 | | - fClose = sqlite3_strglob("*_close*", g.zPath)==0; |
| 1669 | | - if( fClose ) zReason = PD("reason",0); |
| 1670 | | - forumpost_tag(fpid, "closed", fClose, zReason); |
| 1671 | | - cgi_redirectf("%R/forumpost/%S",zFpid); |
| 1672 | | - return; |
| 1702 | + }else{ |
| 1703 | + const int bIsAdd = sqlite3_strglob("*_close*", g.zPath)==0; |
| 1704 | + char const *zReason = bIsAdd ? 0 : PD("reason", 0); |
| 1705 | + forumpost_action_helper("closed", zReason, bIsAdd); |
| 1706 | + } |
| 1707 | +} |
| 1708 | + |
| 1709 | +/* |
| 1710 | +** WEBPAGE: forumpost_pin hidden |
| 1711 | +** WEBPAGE: forumpost_unpin hidden |
| 1712 | +** |
| 1713 | +** fpid=X Hash of the post to be edited. REQUIRED |
| 1714 | +** |
| 1715 | +** Pins or unpins the given forum post, within the bounds of the |
| 1716 | +** API for forumpost_tag(). After (perhaps) modifying the "pinned" |
| 1717 | +** tag of the given thread, it redirects to that post's thread |
| 1718 | +** view. Requires setup privileges. |
| 1719 | +*/ |
| 1720 | +void forum_page_pin(void){ |
| 1721 | + login_check_credentials(); |
| 1722 | + if( !g.perm.Setup ){ |
| 1723 | + login_needed(g.anon.Setup); |
| 1724 | + }else{ |
| 1725 | + const int bIsAdd = sqlite3_strglob("*_pin*", g.zPath)==0; |
| 1726 | + forumpost_action_helper("pinned", 0, bIsAdd); |
| 1727 | + } |
| 1673 | 1728 | } |
| 1674 | 1729 | |
| 1675 | 1730 | /* |
| 1676 | 1731 | ** WEBPAGE: forumnew |
| 1677 | 1732 | ** WEBPAGE: forumedit |
| | @@ -2241,42 +2296,53 @@ |
| 2241 | 2296 | style_submenu_entry("n","Max:",4,0); |
| 2242 | 2297 | iOfst = atoi(PD("x","0")); |
| 2243 | 2298 | iCnt = 0; |
| 2244 | 2299 | if( db_table_exists("repository","forumpost") ){ |
| 2245 | 2300 | db_prepare(&q, |
| 2246 | | - "WITH thread(age,duration,cnt,root,last) AS (" |
| 2301 | + "WITH thread(age,duration,cnt,root,last,sticky) AS (" |
| 2247 | 2302 | " SELECT" |
| 2248 | 2303 | " julianday('now') - max(fmtime)," |
| 2249 | 2304 | " max(fmtime) - min(fmtime)," |
| 2250 | 2305 | " sum(fprev IS NULL)," |
| 2251 | 2306 | " froot," |
| 2252 | 2307 | " (SELECT fpid FROM forumpost AS y" |
| 2253 | 2308 | " WHERE y.froot=x.froot %s" |
| 2254 | | - " ORDER BY y.fmtime DESC LIMIT 1)" |
| 2309 | + " ORDER BY y.fmtime DESC LIMIT 1)," |
| 2310 | + " CASE WHEN" |
| 2311 | + " firt IS NULL AND" |
| 2312 | + " (SELECT 1 FROM tagxref ref, tag t" |
| 2313 | + " WHERE ref.rid=x.fpid AND ref.tagtype>0" |
| 2314 | + " AND ref.tagid=t.tagid" |
| 2315 | + " AND t.tagname='pinned')" |
| 2316 | + " THEN 1" |
| 2317 | + " ELSE 0" |
| 2318 | + " END" |
| 2255 | 2319 | " FROM forumpost AS x" |
| 2256 | 2320 | " WHERE %s" |
| 2257 | 2321 | " GROUP BY froot" |
| 2258 | | - " ORDER BY 1 LIMIT %d OFFSET %d" |
| 2322 | + " ORDER BY 6 DESC, 1 LIMIT %d OFFSET %d" |
| 2259 | 2323 | ")" |
| 2260 | 2324 | "SELECT" |
| 2261 | 2325 | " thread.age," /* 0 */ |
| 2262 | 2326 | " thread.duration," /* 1 */ |
| 2263 | 2327 | " thread.cnt," /* 2 */ |
| 2264 | 2328 | " blob.uuid," /* 3 */ |
| 2265 | 2329 | " substr(event.comment,instr(event.comment,':')+1)," /* 4 */ |
| 2266 | | - " thread.last" /* 5 */ |
| 2330 | + " thread.last," /* 5 */ |
| 2331 | + " thread.sticky" /* 6 */ |
| 2267 | 2332 | " FROM thread, blob, event" |
| 2268 | 2333 | " WHERE blob.rid=thread.last" |
| 2269 | 2334 | " AND event.objid=thread.last" |
| 2270 | | - " ORDER BY 1;", |
| 2335 | + " ORDER BY 7 DESC, 1;", |
| 2271 | 2336 | g.perm.ModForum ? "" : "AND y.fpid NOT IN private" /*safe-for-%s*/, |
| 2272 | 2337 | g.perm.ModForum ? "true" : "fpid NOT IN private" /*safe-for-%s*/, |
| 2273 | 2338 | iLimit+1, iOfst |
| 2274 | 2339 | ); |
| 2275 | 2340 | while( db_step(&q)==SQLITE_ROW ){ |
| 2276 | 2341 | char *zAge = human_readable_age(db_column_double(&q,0)); |
| 2277 | 2342 | int nMsg = db_column_int(&q, 2); |
| 2343 | + int bSticky = db_column_int(&q, 6); |
| 2278 | 2344 | const char *zUuid = db_column_text(&q, 3); |
| 2279 | 2345 | const char *zTitle = db_column_text(&q, 4); |
| 2280 | 2346 | if( iCnt==0 ){ |
| 2281 | 2347 | if( iOfst>0 ){ |
| 2282 | 2348 | @ <h1>Threads at least %s(zAge) old</h1> |
| | @@ -2301,11 +2367,11 @@ |
| 2301 | 2367 | @ %z(href("%R/forum?x=%d&n=%d",iOfst+iLimit,iLimit))\ |
| 2302 | 2368 | @ ↓ Older...</a></td></tr> |
| 2303 | 2369 | fossil_free(zAge); |
| 2304 | 2370 | break; |
| 2305 | 2371 | } |
| 2306 | | - @ <tr><td>%h(zAge) ago</td> |
| 2372 | + @ <tr%s(bSticky ? " class='sticky'" : "")><td>%h(zAge) ago</td> |
| 2307 | 2373 | @ <td>%z(href("%R/forumpost/%S",zUuid))%h(zTitle)</a></td> |
| 2308 | 2374 | @ <td>\ |
| 2309 | 2375 | if( g.perm.ModForum && moderation_pending(db_column_int(&q,5)) ){ |
| 2310 | 2376 | @ <span class="modpending">\ |
| 2311 | 2377 | @ Awaiting Moderator Approval</span><br> |
| 2312 | 2378 | |