video_view.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. const { form, button, div, h2, p, section, input, label, br, a, video: videoHyperaxe, span, textarea } = require("../server/node_modules/hyperaxe");
  2. const moment = require("../server/node_modules/moment");
  3. const { template, i18n } = require('./main_views');
  4. const { config } = require('../server/SSB_server.js');
  5. const { renderUrl } = require('../backend/renderUrl');
  6. const opinionCategories = require('../backend/opinion_categories');
  7. const userId = config.keys.id;
  8. const getFilteredVideos = (filter, videos, userId) => {
  9. const now = Date.now();
  10. let filtered =
  11. filter === 'mine' ? videos.filter(v => v.author === userId) :
  12. filter === 'recent' ? videos.filter(v => new Date(v.createdAt).getTime() >= now - 86400000) :
  13. filter === 'top' ? [...videos].sort((a, b) => {
  14. const sumA = Object.values(a.opinions || {}).reduce((s, n) => s + (n || 0), 0);
  15. const sumB = Object.values(b.opinions || {}).reduce((s, n) => s + (n || 0), 0);
  16. return sumB - sumA;
  17. }) :
  18. videos;
  19. return filtered.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
  20. };
  21. const renderVideoCommentsSection = (videoId, comments = []) => {
  22. const commentsCount = Array.isArray(comments) ? comments.length : 0;
  23. return div({ class: 'vote-comments-section' },
  24. div({ class: 'comments-count' },
  25. span({ class: 'card-label' }, i18n.voteCommentsLabel + ': '),
  26. span({ class: 'card-value' }, String(commentsCount))
  27. ),
  28. div({ class: 'comment-form-wrapper' },
  29. h2({ class: 'comment-form-title' }, i18n.voteNewCommentLabel),
  30. form({
  31. method: 'POST',
  32. action: `/videos/${encodeURIComponent(videoId)}/comments`,
  33. class: 'comment-form'
  34. },
  35. textarea({
  36. id: 'comment-text',
  37. name: 'text',
  38. required: true,
  39. rows: 4,
  40. class: 'comment-textarea',
  41. placeholder: i18n.voteNewCommentPlaceholder
  42. }),
  43. br(),
  44. button({ type: 'submit', class: 'comment-submit-btn' }, i18n.voteNewCommentButton)
  45. )
  46. ),
  47. comments && comments.length
  48. ? div({ class: 'comments-list' },
  49. comments.map(c => {
  50. const author = c.value && c.value.author ? c.value.author : '';
  51. const ts = c.value && c.value.timestamp ? c.value.timestamp : c.timestamp;
  52. const absDate = ts ? moment(ts).format('YYYY/MM/DD HH:mm:ss') : '';
  53. const relDate = ts ? moment(ts).fromNow() : '';
  54. const userName = author && author.includes('@') ? author.split('@')[1] : author;
  55. return div({ class: 'votations-comment-card' },
  56. span({ class: 'created-at' },
  57. span(i18n.createdBy),
  58. author
  59. ? a(
  60. { href: `/author/${encodeURIComponent(author)}` },
  61. `@${userName}`
  62. )
  63. : span('(unknown)'),
  64. absDate ? span(' | ') : '',
  65. absDate ? span({ class: 'votations-comment-date' }, absDate) : '',
  66. relDate ? span({ class: 'votations-comment-date' }, ' | ', i18n.sendTime) : '',
  67. relDate
  68. ? a(
  69. {
  70. href: `/thread/${encodeURIComponent(c.value.content.fork || c.value.content.root)}#${encodeURIComponent(c.key)}`
  71. },
  72. relDate
  73. )
  74. : ''
  75. ),
  76. p({
  77. class: 'votations-comment-text',
  78. innerHTML: (c.value && c.value.content && c.value.content.text) || ''
  79. })
  80. );
  81. })
  82. )
  83. : p({ class: 'votations-no-comments' }, i18n.voteNoCommentsYet)
  84. );
  85. };
  86. const renderVideoActions = (filter, video) => {
  87. return filter === 'mine' ? div({ class: "video-actions" },
  88. form({ method: "GET", action: `/videos/edit/${encodeURIComponent(video.key)}` },
  89. button({ class: "update-btn", type: "submit" }, i18n.videoUpdateButton)
  90. ),
  91. form({ method: "POST", action: `/videos/delete/${encodeURIComponent(video.key)}` },
  92. button({ class: "delete-btn", type: "submit" }, i18n.videoDeleteButton)
  93. )
  94. ) : null;
  95. };
  96. const renderVideoList = (filteredVideos, filter) => {
  97. return filteredVideos.length > 0
  98. ? filteredVideos.map(video => {
  99. const commentCount = typeof video.commentCount === 'number' ? video.commentCount : 0;
  100. return div({ class: "tags-header" },
  101. renderVideoActions(filter, video),
  102. form({ method: "GET", action: `/videos/${encodeURIComponent(video.key)}` },
  103. button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
  104. ),
  105. video.title?.trim() ? h2(video.title) : null,
  106. video.url
  107. ? div({ class: "video-container" },
  108. videoHyperaxe({
  109. controls: true,
  110. src: `/blob/${encodeURIComponent(video.url)}`,
  111. type: video.mimeType,
  112. preload: 'metadata',
  113. width: '640',
  114. height: '360'
  115. })
  116. )
  117. : p(i18n.videoNoFile),
  118. video.description?.trim() ? p(...renderUrl(video.description)) : null,
  119. video.tags?.length
  120. ? div({ class: "card-tags" },
  121. video.tags.map(tag =>
  122. a(
  123. {
  124. href: `/search?query=%23${encodeURIComponent(tag)}`,
  125. class: "tag-link"
  126. },
  127. `#${tag}`
  128. )
  129. )
  130. )
  131. : null,
  132. div({ class: 'card-comments-summary' },
  133. span({ class: 'card-label' }, i18n.voteCommentsLabel + ':'),
  134. span({ class: 'card-value' }, String(commentCount)),
  135. br, br,
  136. form({ method: 'GET', action: `/videos/${encodeURIComponent(video.key)}` },
  137. button({ type: 'submit', class: 'filter-btn' }, i18n.voteCommentsForumButton)
  138. )
  139. ),
  140. br,
  141. p({ class: 'card-footer' },
  142. span({ class: 'date-link' }, `${moment(video.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
  143. a({ href: `/author/${encodeURIComponent(video.author)}`, class: 'user-link' }, `${video.author}`)
  144. ),
  145. div({ class: "voting-buttons" },
  146. opinionCategories.map(category =>
  147. form({ method: "POST", action: `/videos/opinions/${encodeURIComponent(video.key)}/${category}` },
  148. button(
  149. { class: "vote-btn" },
  150. `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`] || category} [${video.opinions?.[category] || 0}]`
  151. )
  152. )
  153. )
  154. )
  155. );
  156. })
  157. : div(i18n.noVideos);
  158. };
  159. const renderVideoForm = (filter, videoId, videoToEdit) => {
  160. return div({ class: "div-center video-form" },
  161. form({
  162. action: filter === 'edit' ? `/videos/update/${encodeURIComponent(videoId)}` : "/videos/create",
  163. method: "POST", enctype: "multipart/form-data"
  164. },
  165. label(i18n.videoFileLabel), br(),
  166. input({ type: "file", name: "video", required: filter !== "edit" }), br(), br(),
  167. label(i18n.videoTagsLabel), br(),
  168. input({ type: "text", name: "tags", placeholder: i18n.videoTagsPlaceholder, value: videoToEdit?.tags?.join(', ') || '' }), br(), br(),
  169. label(i18n.videoTitleLabel), br(),
  170. input({ type: "text", name: "title", placeholder: i18n.videoTitlePlaceholder, value: videoToEdit?.title || '' }), br(), br(),
  171. label(i18n.videoDescriptionLabel), br(),
  172. textarea({ name: "description", placeholder: i18n.videoDescriptionPlaceholder, rows: "4", value: videoToEdit?.description || '' }), br(), br(),
  173. button({ type: "submit" }, filter === 'edit' ? i18n.videoUpdateButton : i18n.videoCreateButton)
  174. )
  175. );
  176. };
  177. exports.videoView = async (videos, filter, videoId) => {
  178. const title = filter === 'mine' ? i18n.videoMineSectionTitle :
  179. filter === 'create' ? i18n.videoCreateSectionTitle :
  180. filter === 'edit' ? i18n.videoUpdateSectionTitle :
  181. filter === 'recent' ? i18n.videoRecentSectionTitle :
  182. filter === 'top' ? i18n.videoTopSectionTitle :
  183. i18n.videoAllSectionTitle;
  184. const filteredVideos = getFilteredVideos(filter, videos, userId);
  185. const videoToEdit = videos.find(v => v.key === videoId);
  186. return template(
  187. title,
  188. section(
  189. div({ class: "tags-header" },
  190. h2(title),
  191. p(i18n.videoDescription)
  192. ),
  193. div({ class: "filters" },
  194. form({ method: "GET", action: "/videos" },
  195. ["all", "mine", "recent", "top"].map(f =>
  196. button({
  197. type: "submit", name: "filter", value: f,
  198. class: filter === f ? "filter-btn active" : "filter-btn"
  199. },
  200. i18n[`videoFilter${f.charAt(0).toUpperCase() + f.slice(1)}`]
  201. )
  202. ),
  203. button({ type: "submit", name: "filter", value: "create", class: "create-button" },
  204. i18n.videoCreateButton)
  205. )
  206. )
  207. ),
  208. section(
  209. (filter === 'create' || filter === 'edit')
  210. ? renderVideoForm(filter, videoId, videoToEdit)
  211. : renderVideoList(filteredVideos, filter)
  212. )
  213. );
  214. };
  215. exports.singleVideoView = async (video, filter, comments = []) => {
  216. const isAuthor = video.author === userId;
  217. const hasOpinions = Object.keys(video.opinions || {}).length > 0;
  218. return template(
  219. i18n.videoTitle,
  220. section(
  221. div({ class: "filters" },
  222. form({ method: "GET", action: "/videos" },
  223. button({ type: "submit", name: "filter", value: "all", class: filter === 'all' ? 'filter-btn active' : 'filter-btn' }, i18n.videoFilterAll),
  224. button({ type: "submit", name: "filter", value: "mine", class: filter === 'mine' ? 'filter-btn active' : 'filter-btn' }, i18n.videoFilterMine),
  225. button({ type: "submit", name: "filter", value: "recent", class: filter === 'recent' ? 'filter-btn active' : 'filter-btn' }, i18n.videoFilterRecent),
  226. button({ type: "submit", name: "filter", value: "top", class: filter === 'top' ? 'filter-btn active' : 'filter-btn' }, i18n.videoFilterTop),
  227. button({ type: "submit", name: "filter", value: "create", class: "create-button" }, i18n.videoCreateButton)
  228. )
  229. ),
  230. div({ class: "tags-header" },
  231. isAuthor ? div({ class: "video-actions" },
  232. !hasOpinions
  233. ? form({ method: "GET", action: `/videos/edit/${encodeURIComponent(video.key)}` },
  234. button({ class: "update-btn", type: "submit" }, i18n.videoUpdateButton)
  235. )
  236. : null,
  237. form({ method: "POST", action: `/videos/delete/${encodeURIComponent(video.key)}` },
  238. button({ class: "delete-btn", type: "submit" }, i18n.videoDeleteButton)
  239. )
  240. ) : null,
  241. h2(video.title),
  242. video.url
  243. ? div({ class: "video-container" },
  244. videoHyperaxe({
  245. controls: true,
  246. src: `/blob/${encodeURIComponent(video.url)}`,
  247. type: video.mimeType,
  248. preload: 'metadata',
  249. width: '640',
  250. height: '360'
  251. })
  252. )
  253. : p(i18n.videoNoFile),
  254. p(...renderUrl(video.description)),
  255. video.tags?.length
  256. ? div({ class: "card-tags" },
  257. video.tags.map(tag =>
  258. a(
  259. {
  260. href: `/search?query=%23${encodeURIComponent(tag)}`,
  261. class: "tag-link"
  262. },
  263. `#${tag}`
  264. )
  265. )
  266. )
  267. : null,
  268. br,
  269. p({ class: 'card-footer' },
  270. span({ class: 'date-link' }, `${moment(video.createdAt).format('YYYY/MM/DD HH:mm:ss')} ${i18n.performed} `),
  271. a({ href: `/author/${encodeURIComponent(video.author)}`, class: 'user-link' }, `${video.author}`)
  272. )
  273. ),
  274. div({ class: "voting-buttons" },
  275. opinionCategories.map(category =>
  276. form({ method: "POST", action: `/videos/opinions/${encodeURIComponent(video.key)}/${category}` },
  277. button(
  278. { class: "vote-btn" },
  279. `${i18n[`vote${category.charAt(0).toUpperCase() + category.slice(1)}`] || category} [${video.opinions?.[category] || 0}]`
  280. )
  281. )
  282. )
  283. ),
  284. renderVideoCommentsSection(video.key, comments)
  285. )
  286. );
  287. };