Compare commits

...

182 Commits

Author SHA1 Message Date
  欧醚 20e797a51d fix permission 1 year ago
  欧醚 9a1e8d0f4b Merge tag 'v3.4.6' into cs+3.4.6 2 years ago
  欧醚 4390b676da 加强邮箱保护 2 years ago
  欧醚 a9554a1c9b 展示转嘟,替代 1d0ddd33b6 的临时措施 2 years ago
  Claire 93a6c143af
Fix insufficient sanitization of report comments (#17430) 2 years ago
  Claire bb7b2868a0 Bump version to 3.4.6 2 years ago
  Wonderfall a06dda41d0 disable legacy XSS filtering (#17289) 2 years ago
  Claire bf005edd30 Change mastodon:webpush:generate_vapid_key task to not require functional env (#17338) 2 years ago
  Claire df68d2eab8 Fix response_to_recipient? CTE 2 years ago
  Claire b27f50da5a Fix insufficient sanitization of report comments 2 years ago
  Claire e2009ced3a Fix compacted JSON-LD possibly causing compatibility issues on forwarding 2 years ago
  Puck Meerburg fe0210074f Compact JSON-LD signed incoming activities 2 years ago
  Claire c8dbbd60eb Fix error-prone SQL queries (#15828) 2 years ago
  Claire 6d831fe274
Fix spurious errors when receiving an Add activity for a private post (#17425) 2 years ago
  TA 0b7b25d17c remove unrendered HTML b tag from welcome email 2 years ago
  欧醚 eede6ca0c5 change version suffix to fix push notification 2 years ago
  欧醚 fa687d53a6 show toot chars limit and poll limit in instance api (for mobile app) 2 years ago
  欧醚 910de600d5 修改投票选项数上限 2 years ago
  Claire 2476d4f391 Fix user email address being banned on self-deletion (#16503) 2 years ago
  欧醚 34927adb74 兼容不开启闭社树 2 years ago
  欧醚 4db2290525 Merge tag 'v3.4.1' into closed-social-v3 2 years ago
  TA 5dd4f64b9f [fix] background image of THU 2 years ago
  欧醚 1d0ddd33b6 临时措施 2 years ago
  TA 3312ec5e9b strip for anonymous status 3 years ago
  TA 50101ad5f2 temporarily disable security code email 3 years ago
  TA 14630f3f48 fix hour format temporarily 3 years ago
  欧醚 fbf63eef29 Merge v3.3.0 into closed-social-v3 3 years ago
  欧醚 09c1e377a7 fix detail 3 years ago
  欧醚 d3625fa0f4 feat: 年度数据 fix some detail & 允许管理员查询其他人 3 years ago
  欧醚 fd8dd1498d feat: 年度数据统计 3 years ago
  欧醚 f0c6d72e60 配置参数说明 3 years ago
  欧醚 84d2e402d3 匿名每天刷新 3 years ago
  欧醚 a583987d76 Merge pull request 'fix bugs' (#5) from use-display-name-for-mention into closed-social-v3 3 years ago
  欧醚 c148e7a041 fix bugs 3 years ago
  欧醚 2b34c72fc4 Merge pull request 'use display_name for mention' (#4) from use-display-name-for-mention into closed-social-v3 3 years ago
  欧醚 ff0da1940b use display_name for mention 3 years ago
  欧醚 0d55e30e9e 修复界面bug 3 years ago
  欧醚 54cf68d8c6 增加前端邮箱正则限制 3 years ago
  欧醚 48cf6d253e make api/v1/instance public 3 years ago
  欧醚 04bfe40736 Merge remote-tracking branch 'origin/master' into closed-social-v3 3 years ago
  Junxi Song 3140a72fd7 Fixed the problem of fixed comments. 3 years ago
  欧醚 2cbee8feda 删除空格 3 years ago
  欧醚 53e44cc118 删除关注tag功能 3 years ago
  欧醚 eee3899b2c Merge remote-tracking branch 'origin/master' into closed-social-v3 3 years ago
  Junxi Song 4b80abced1 更新英文,台湾,香港表述# 3 years ago
  欧醚 bdb623f9eb 版本号后缀及默认仓库 3 years ago
  欧醚 98fe3af7b2 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 38632675b1 fix bugs 3 years ago
  欧醚 aa139841e5 恢复界面 3 years ago
  欧醚 b33a2295a9 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 f4e504e1fb 修复注册页面的显示 3 years ago
  欧醚 445ff0ed5f Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 3b950848ca Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 b6e7399e9e Merge tag 'v3.2.0' into closed-social-v3 3 years ago
  欧醚 7a9863cef7 中文搜索使用ik分词器 3 years ago
  欧醚 2a780b1118 og:image 允许不规范的 image/jpg (兼容网易云音乐) 3 years ago
  欧醚 c96e2c1d8f 跨站栏显示热门 3 years ago
  欧醚 57f5128fc6 fix bug (static variables are not thread safe) 3 years ago
  欧醚 d67cd9a573 微调界面 3 years ago
  欧醚 1b666873a2 允许whitelist_mode显示更多内容 3 years ago
  欧醚 c4897a3157 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 89bc52e250 微调about界面 3 years ago
  欧醚 bd9607f5c6 preview card 兼容有重复content-type头的情况 3 years ago
  欧醚 478efa8bf9 fix bug 3 years ago
  欧醚 20e0c1f803 重新实现了获取context 3 years ago
  欧醚 8ee92eab43 优化UI 3 years ago
  欧醚 a93e09ed9d 缩短加载评论延时,对所有嘟文加载评论 3 years ago
  欧醚 d39498f027 删除了所有关于清华邮箱延迟的提醒 3 years ago
  欧醚 0646238fed fix bug of mastodon(?): add :ssl in smtp setting 3 years ago
  欧醚 3dfac8387d fix bugs && 匿名可禁言 3 years ago
  欧醚 747ac90294 fix bug && 取消不必要的修改 3 years ago
  欧醚 57c905a689 minor change 3 years ago
  欧醚 6e1eb19ae0 删除硬编码ws地址 3 years ago
  欧醚 192657c54b 标记匿名标签刷新 超过一天未使用自动刷新 3 years ago
  欧醚 32cb3aa8d9 闭社树树根帐号可配置 对所有嘟文提供树形结构 3 years ago
  欧醚 720f8fc9d1 fix bug 3 years ago
  欧醚 95ff45bb7d 更好的匿名 3 years ago
  欧醚 cdf0e96549 优化了markdown外链图片的显示格式 3 years ago
  欧醚 24d6a043e5 删除markdown库,修改linkify实现嘟文/公告支持markdown语法的链接,彻底清理置顶栏 3 years ago
  欧醚 3149fbd706 删除置顶栏目,公告使用markdown 3 years ago
  欧醚 c247330f05 Merge branch 'master' of https://github.com/tootsuite/mastodon into closed-social-v3 3 years ago
  欧醚 3cc3992d1d 删除orig 修改细节 匿名可配置 3 years ago
  欧醚 9312eab8de fix bug && 小改动 3 years ago
  欧醚 d5cc1beb73 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 7f54b403de 细节 4 years ago
  欧醚 4eb82a685f fix bugs 4 years ago
  欧醚 2a094835e9 清华紫主题恢复显示吉祥物 4 years ago
  欧醚 0b5b741e32 jump 处理参数 4 years ago
  欧醚 a0c0005808 fix bugs 4 years ago
  欧醚 bc3df81610 修改jump跳转 4 years ago
  欧醚 09688a0535 Merge branch 'closed-social-v3' of github.com:closed-social/mastodon into closed-social-v3 4 years ago
  欧醚 d7400b4d34 /jump 登陆后跳转功能 4 years ago
  欧醚 ca716e07ef mask不同天使用不同的编码规则 4 years ago
  欧醚 b323bc8e15 默认白名单模式,修改apple icon 4 years ago
  欧醚 97cfee7886 白名单模式下仍然提供about about/more 页面 4 years ago
  欧醚 b39c079cec 匿名使用汉字编号 4 years ago
  欧醚 7848876404 每日刷新匿名编号 4 years ago
  欧醚 459942e2ea 允许任何时候匿名 4 years ago
  欧醚 bb9c5f822e 匿名评论有简单数字代号 4 years ago
  欧醚 ac744cd462 取消错误的修改(直接使用mastodon的白名单模式控制权限),针对微信内置浏览器跳转 4 years ago
  欧醚 dbd802d13b 修改闭社树上内容转嘟的引用显示 4 years ago
  欧醚 29c865dbc8 fix bugs 4 years ago
  欧醚 aeb8046cb5 fix bugs;闭社树按时间倒序 4 years ago
  欧醚 c608897a29 Merge branch 'closed-social-v3' of github.com:closed-social/mastodon into closed-social-v3 4 years ago
  欧醚 4f1f2540a4 评论预览显示两层,微调显示界面 4 years ago
  欧醚 3197fd21e4 修复修改邮箱时不检查白名单的问题 4 years ago
  欧醚 40533e194f 调整了左右滑动的阈值 4 years ago
  欧醚 0efe1c54e9 为ws单独指定域名(解决CDN不支持ws的问题),放宽api频率 4 years ago
  欧醚 d00d6415fe Fix reuse of detailed status components 4 years ago
  欧醚 4103b2a466 重新整理了评论/回复图标及文字 4 years ago
  欧醚 e701ceb033 删除了旧的点赞动画 4 years ago
  欧醚 2bb0aaa47c Merge branch 'closed-social-v3' of https://github.com/closed-social/mastodon into closed-social-v3 4 years ago
  欧醚 e389037482 修改了喜欢的图标颜色和动画 4 years ago
  欧醚 33e1c8680e 闭社树根节点只加载一层 4 years ago
  欧醚 6bd0848a0b 修改frame规则 4 years ago
  欧醚 dd16a8f4ac 描述为https链接的的图片显示为iframe 4 years ago
  欧醚 604cffbeed fix bugs 4 years ago
  欧醚 112a3f0754 时间轴只显示对自己的转嘟 4 years ago
  欧醚 0414dccfe9 闭社树每次只加载两层 4 years ago
  欧醚 ec25fe716e 不对不可转嘟问显示转嘟数,private嘟文强制请求context 4 years ago
  欧醚 50439d7218 延时自动加载评论,不再需要鼠标经过/点击 4 years ago
  欧醚 f8a2a10284 显示引用以实现转评 4 years ago
  欧醚 78bacf9626 取消热门tag门槛,清华邮箱的额外提醒信息 4 years ago
  欧醚 49a2a2ef96 修复bug 4 years ago
  欧醚 dc6403acac 为高级web模式设置置顶栏 4 years ago
  欧醚 0dbbca12c0 修改文字,解决previewcard被反爬虫的问题,增长Timeout 4 years ago
  欧醚 56f55defc2 修改了移动应用、指南(文档)的链接,修改了消息图标 4 years ago
  欧醚 7a02ce51be 修改了评论预览的结构和显示方式(增加了wrapper) 4 years ago
  欧醚 774235c1f5 修改了评论预览的展开方式 4 years ago
  欧醚 08e12eadaf 修改默认头像,修改style 4 years ago
  欧醚 36ec3f043c 修改style,置顶栏可关闭 4 years ago
  欧醚 b9541d6adc 修复style 4 years ago
  欧醚 be62d2550f 修复背景图显示 4 years ago
  欧醚 c4ff322c3a 新增主题,并迁移部分自定义css到主题 4 years ago
  欧醚 1c90891868 置顶栏支持富文本 4 years ago
  欧醚 d2d99f86dd 仅[pub]开头的嘟文站外可见 4 years ago
  欧醚 e5c6a02ae4 修复favicon, 为所有评论显示show thread 4 years ago
  欧醚 f9fe84914a 不在时间线显示自回复 4 years ago
  欧醚 c67bf258ab 修改图标 4 years ago
  欧醚 c2dc7566c1 置顶消息,回复API限制,修改闭社树地址配置方式 4 years ago
  欧醚 9b76f5bb43 优化了闭社树的显示 4 years ago
  欧醚 eae877d063 修改了评论预览的处理逻辑,移动端第一次点击加载评论预览,第二次点击打开嘟文 4 years ago
  欧醚 1db7d22058 修改后端 赞藏 -> 喜欢 4 years ago
  欧醚 3b326e3a55 字数限制 500 -> 5000 4 years ago
  欧醚 24859251dc 本站栏显示热门tag榜 4 years ago
  欧醚 6a4bfbf621 禁止非closed.social的mastodon实例 Warning:硬编码 4 years ago
  欧醚 a31aaeef04 change email info and force to use zh-CN 4 years ago
  欧醚 8fa6fcf097 仅在鼠标移过/手机按下时加载评论预览 4 years ago
  欧醚 062308c006 放宽了非认证api频率限制 4 years ago
  欧醚 a83a3e78fc 放宽了api频率限制 4 years ago
  欧醚 a04935f5c1 树洞评论匿名 4 years ago
  欧醚 66561200b5 show comments on timeline, and show right count when unfavourite/unreblog 4 years ago
  欧醚 3142db432d set default email domain in ENV 4 years ago
  欧醚 5b9cc9c93b change 赞藏 to 喜欢, and change logo_alt 4 years ago
  欧醚 718814bfbf give up pinned statuses, and add tree to navigation/tabs_bar, with funny way 4 years ago
  欧醚 713bf5509f add 闭社树 to navigation, with horrible way 4 years ago
  欧醚 40ea60e578 show reblogs on timeline 4 years ago
  欧醚 6dd757a495 change x and y of text on tree 4 years ago
  欧醚 4f2275c1e5 click the node on tree to go to the status 4 years ago
  欧醚 0a8bcfade5 fix bug for tree animate. Use statusId as keyPro 4 years ago
  欧醚 fb82f7b6ac now you can click nodes on 闭社树 4 years ago
  欧醚 2bb6238dc8 change size of tree graph 4 years ago
  欧醚 a8e4c3f5a2 now can draw 闭社树 4 years ago
  欧醚 6be4def2ec follow tags 4 years ago
  欧醚 0101dfcdfd fix bug 4 years ago
  欧醚 a5db5d45a2 fix bug 4 years ago
  欧醚 b0aadae668 show tree in className 4 years ago
  欧醚 f0ad5eaea7 pin statuses pinned by Account(1) 4 years ago
  欧醚 353b88eba0 require login for tag api 4 years ago
  欧醚 3fd2acbacb show depth and only show direct sons for 闭社树 4 years ago
  欧醚 457daf4353 better hide to unsigned user 4 years ago
  欧醚 60a876f39a change 收藏 to 赞藏, and hide wrong active_user_count 4 years ago
  欧醚 489c93f011 show comments on timeline, with ugly way 4 years ago
  欧醚 3c25a96aad show comments on timeline, with bugs 4 years ago
  欧醚 990d264f63 show fav/reb number 4 years ago
  欧醚 5eb86661ba set max trends number to 5 4 years ago
  欧醚 207b875ded hide account info to unsigned user 4 years ago
  欧醚 3f178bbdae change all icon of fav/rep 4 years ago
  欧醚 66919577e8 change some icon of fav/rep 4 years ago
  欧醚 c2c5c22ee6 fix bugs 4 years ago
  欧醚 79f21b2d8f fix favicon.ico 4 years ago
  欧醚 1abf9c6d92 merge changes 4 years ago
123 changed files with 2361 additions and 403 deletions
Unified View
  1. +6
    -0
      .gitignore
  2. +12
    -0
      CHANGELOG.md
  3. +30
    -0
      CS_CONF.md
  4. +1
    -0
      app/chewy/statuses_index.rb
  5. +35
    -1
      app/controllers/about_controller.rb
  6. +2
    -2
      app/controllers/api/v1/instances/activity_controller.rb
  7. +2
    -2
      app/controllers/api/v1/instances/peers_controller.rb
  8. +1
    -1
      app/controllers/api/v1/instances_controller.rb
  9. +17
    -3
      app/controllers/api/v1/statuses_controller.rb
  10. +2
    -1
      app/controllers/api/v1/timelines/public_controller.rb
  11. +1
    -1
      app/controllers/home_controller.rb
  12. +7
    -1
      app/helpers/application_helper.rb
  13. +55
    -0
      app/helpers/context_helper.rb
  14. +10
    -2
      app/helpers/home_helper.rb
  15. +80
    -0
      app/helpers/jsonld_helper.rb
  16. +1
    -1
      app/javascript/images/logo.svg
  17. +1
    -1
      app/javascript/images/logo_alt.svg
  18. +5
    -1
      app/javascript/images/logo_full.svg
  19. +1
    -1
      app/javascript/images/logo_transparent.svg
  20. +1
    -1
      app/javascript/images/logo_transparent_black.svg
  21. BIN
     
  22. BIN
     
  23. +8
    -2
      app/javascript/mastodon/actions/interactions.js
  24. +4
    -1
      app/javascript/mastodon/actions/timelines.js
  25. +12
    -1
      app/javascript/mastodon/components/media_gallery.js
  26. +80
    -4
      app/javascript/mastodon/components/status.js
  27. +473
    -0
      app/javascript/mastodon/components/status2.js
  28. +7
    -6
      app/javascript/mastodon/components/status_action_bar.js
  29. +1
    -1
      app/javascript/mastodon/components/status_content.js
  30. +64
    -4
      app/javascript/mastodon/containers/status_container.js
  31. +168
    -0
      app/javascript/mastodon/containers/status_container2.js
  32. +2
    -2
      app/javascript/mastodon/features/compose/components/compose_form.js
  33. +1
    -1
      app/javascript/mastodon/features/compose/components/poll_form.js
  34. +16
    -1
      app/javascript/mastodon/features/compose/index.js
  35. +1
    -1
      app/javascript/mastodon/features/favourited_statuses/index.js
  36. +1
    -1
      app/javascript/mastodon/features/getting_started/components/trends.js
  37. +1
    -1
      app/javascript/mastodon/features/getting_started/index.js
  38. +2
    -2
      app/javascript/mastodon/features/notifications/components/filter_bar.js
  39. +1
    -1
      app/javascript/mastodon/features/notifications/components/notification.js
  40. +6
    -5
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  41. +9
    -6
      app/javascript/mastodon/features/status/components/action_bar.js
  42. +18
    -5
      app/javascript/mastodon/features/status/components/detailed_status.js
  43. +94
    -11
      app/javascript/mastodon/features/status/index.js
  44. +6
    -0
      app/javascript/mastodon/features/ui/components/columns_area.js
  45. +1
    -1
      app/javascript/mastodon/features/ui/components/link_footer.js
  46. +5
    -2
      app/javascript/mastodon/features/ui/components/navigation_panel.js
  47. +15
    -1
      app/javascript/mastodon/features/ui/components/tabs_bar.js
  48. +3
    -0
      app/javascript/mastodon/initial_state.js
  49. +5
    -2
      app/javascript/mastodon/locales/en.json
  50. +5
    -2
      app/javascript/mastodon/locales/zh-CN.json
  51. +11
    -8
      app/javascript/mastodon/locales/zh-HK.json
  52. +15
    -11
      app/javascript/mastodon/locales/zh-TW.json
  53. +4
    -0
      app/javascript/styles/application.scss
  54. +115
    -0
      app/javascript/styles/closed-social/global.scss
  55. +39
    -0
      app/javascript/styles/closed-social/timeline_comments.scss
  56. +65
    -0
      app/javascript/styles/closed-social/tree.scss
  57. +1
    -0
      app/javascript/styles/mastodon-light/variables.scss
  58. +2
    -1
      app/javascript/styles/mastodon/about.scss
  59. +1
    -1
      app/javascript/styles/mastodon/forms.scss
  60. +4
    -1
      app/javascript/styles/mastodon/variables.scss
  61. +3
    -0
      app/javascript/styles/thu.scss
  62. +221
    -0
      app/javascript/styles/thu/diff.scss
  63. +8
    -0
      app/javascript/styles/thu/variables.scss
  64. +1
    -1
      app/lib/activitypub/activity/add.rb
  65. +2
    -50
      app/lib/activitypub/adapter.rb
  66. +36
    -3
      app/lib/formatter.rb
  67. +1
    -1
      app/lib/search_query_transformer.rb
  68. +55
    -50
      app/models/account.rb
  69. +1
    -1
      app/models/preview_card.rb
  70. +4
    -6
      app/models/status.rb
  71. +2
    -2
      app/models/trending_tags.rb
  72. +3
    -2
      app/models/user.rb
  73. +1
    -0
      app/policies/user_policy.rb
  74. +2
    -0
      app/serializers/initial_state_serializer.rb
  75. +15
    -1
      app/serializers/rest/instance_serializer.rb
  76. +17
    -1
      app/services/activitypub/process_collection_service.rb
  77. +1
    -0
      app/services/fetch_link_card_service.rb
  78. +9
    -5
      app/services/notify_service.rb
  79. +1
    -1
      app/validators/blacklisted_email_validator.rb
  80. +1
    -1
      app/validators/poll_validator.rb
  81. +1
    -1
      app/validators/status_length_validator.rb
  82. +6
    -2
      app/views/about/_registration.html.haml
  83. +11
    -0
      app/views/about/jump.html.haml
  84. +55
    -0
      app/views/about/my_data.html.haml
  85. +2
    -5
      app/views/about/show.html.haml
  86. +3
    -5
      app/views/admin/accounts/show.html.haml
  87. +1
    -1
      app/views/admin/reports/show.html.haml
  88. +11
    -11
      app/views/admin/settings/edit.html.haml
  89. +6
    -2
      app/views/auth/registrations/new.html.haml
  90. +4
    -4
      app/views/follower_accounts/index.html.haml
  91. +4
    -4
      app/views/following_accounts/index.html.haml
  92. +1
    -1
      app/views/home/index.html.haml
  93. +0
    -0
     
  94. +2
    -2
      app/views/layouts/public.html.haml
  95. +3
    -3
      app/views/statuses/_detailed_status.html.haml
  96. +3
    -3
      app/views/statuses/_simple_status.html.haml
  97. +1
    -1
      app/views/statuses/show.html.haml
  98. +1
    -1
      chart/values.yaml
  99. +1
    -1
      config/application.rb
  100. +0
    -80
      config/brakeman.ignore

+ 6
- 0
.gitignore View File

@ -4,6 +4,8 @@
# or operating system, you probably want to add a global ignore instead: # or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global' # git config --global core.excludesfile '~/.gitignore_global'
*.orig
# Ignore bundler config and downloaded libraries. # Ignore bundler config and downloaded libraries.
/.bundle /.bundle
/vendor/bundle /vendor/bundle
@ -65,3 +67,7 @@ yarn-debug.log
# Ignore Docker option files # Ignore Docker option files
docker-compose.override.yml docker-compose.override.yml
# ctag
.ctags
tags

+ 12
- 0
CHANGELOG.md View File

@ -3,6 +3,18 @@ Changelog
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [3.4.6] - 2022-02-03
### Fixed
- Fix `mastodon:webpush:generate_vapid_key` task requiring a functional environment ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17338))
- Fix spurious errors when receiving an Add activity for a private post ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17425))
### Security
- Fix error-prone SQL queries ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/15828))
- Fix not compacting incoming signed JSON-LD activities ([puckipedia](https://github.com/mastodon/mastodon/pull/17426), [ClearlyClaire](https://github.com/mastodon/mastodon/pull/17428)) (CVE-2022-24307)
- Fix insufficient sanitization of report comments ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17430))
- Fix stop condition of a Common Table Expression ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17427))
- Disable legacy XSS filtering ([Wonderfall](https://github.com/mastodon/mastodon/pull/17289))
## [3.4.5] - 2022-01-31 ## [3.4.5] - 2022-01-31
### Added ### Added
- Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393)) - Add more advanced migration tests ([ClearlyClaire](https://github.com/mastodon/mastodon/pull/17393))

+ 30
- 0
CS_CONF.md View File

@ -0,0 +1,30 @@
# 闭社新增配置参数说明
## 匿名相关
闭社新增了匿名发言的功能,开启后,以特定标记结尾(注意不要有多余的回车或空格),发言者就会变成特定的帐号。
`ANON_TAG` 匿名标记,以这个结尾的嘟文就会成为匿名嘟文(本质只是修改了发言者)
`ANON_NAME_LIST` 匿名代号列表,一个文件名,文件内容的格式为每行一个名字。每个用户会随机取一个代号加在匿名嘟文的最前面。对应关系每日更新。
`ANON_ACC` 匿名帐号的id。匿名嘟文的发言者会变成这个帐号。
## 闭社树相关
闭社设计了一套闭社树功能。
`TREE_ADDRESS` 作为闭社树根节点的嘟文的路径(示例:`/statuses/102995298871197915`)
`TREE_ACC`建树帐号的id。跟节点发布自这个帐号的嘟文,在现实上会特殊处理。
## 邮件提示加强
考虑到一定会限制邮箱,闭社加强了邮件提示。
`EMAIL_DEFAULT_DOMAIN` 默认的邮箱后缀,会显示在注册界面并自动补全
`EMAIL_REGEX` 邮箱正则,在前端输入时检查邮箱格式是否正确,这主要是为了及时发现输错邮箱的情况,提升用户体验和减少对邮箱服务器的浪费。
* 注意: 邮箱正则只是一个纯前端的辅助校验,与后端的实际邮箱规则无关。有些时候需要用不公开的邮箱规则创建一些机器人帐号,用于匿名功能的匿名帐号/闭社树功能的建树机器人/管理员帐号/其他bot,邮箱正则会给注册带来不便,可以直接f12打开调试界面直接删除input元素中的正则限制,也可以前期先不使用邮箱正则功能,还可以直接在服务器上通过命令行创建帐号。

+ 1
- 0
app/chewy/statuses_index.rb View File

@ -58,6 +58,7 @@ class StatusesIndex < Chewy::Index
field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do field :text, type: 'text', value: ->(status) { [status.spoiler_text, Formatter.instance.plaintext(status)].concat(status.media_attachments.map(&:description)).concat(status.preloadable_poll ? status.preloadable_poll.options : []).join("\n\n") } do
field :stemmed, type: 'text', analyzer: 'content' field :stemmed, type: 'text', analyzer: 'content'
field :chn , type: 'text', analyzer: 'ik_max_word', search_analyzer: 'ik_smart'
end end
field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) } field :searchable_by, type: 'long', value: ->(status, crutches) { status.searchable_by(crutches) }

+ 35
- 1
app/controllers/about_controller.rb View File

@ -5,11 +5,12 @@ class AboutController < ApplicationController
layout 'public' layout 'public'
before_action :require_open_federation!, only: [:show, :more]
# before_action :require_open_federation!, only: [:show, :more]
before_action :set_body_classes, only: :show before_action :set_body_classes, only: :show
before_action :set_instance_presenter before_action :set_instance_presenter
before_action :set_expires_in, only: [:more, :terms] before_action :set_expires_in, only: [:more, :terms]
before_action :set_registration_form_time, only: :show before_action :set_registration_form_time, only: :show
before_action :authenticate_user!, only: [:jump, :my_data]
skip_before_action :require_functional!, only: [:more, :terms] skip_before_action :require_functional!, only: [:more, :terms]
@ -28,6 +29,39 @@ class AboutController < ApplicationController
def terms; end def terms; end
def jump
@jump_url = "https://#{request.fullpath[6..-1]}"
end
def my_data
@uid = params[:user_id]
if @uid and current_account.user.admin
@account = Account.find(@uid)
else
@account = current_account
end
year = params[:year].to_i
year = nil unless year > 2000
@year_text = year or ''
y = year ? "statuses.created_at >= '#{year}-1-1' and statuses.created_at < '#{year+1}-1-1'" : nil
y2 = year ? "s2.created_at >= '#{year}-1-1' and s2.created_at < '#{year+1}-1-1'" : nil
yf = year ? "favourites.created_at >='#{year}-1-1' and favourites.created_at < '#{year+1}-1-1'" : nil
def raw_to_list(r)
r.map{|k,v| {:account => Account.find(k), :num => v.to_s}}
end
@total = @account.statuses.where(y).count
@most_times = @account.statuses.where(y).group('cast (created_at as date)').reorder('count_id desc').limit(1).count(:id).map{ |k,v| {:date => k.to_s, :num => v.to_s}}
@most_fav = @account.statuses.where(y).joins(:status_stat).reorder('status_stats.favourites_count desc').first
@like_me_most = raw_to_list(@account.statuses.where(yf).joins(:favourites).group('favourites.account_id').reorder('count_id desc').limit(5).count(:id))
@i_like_most = raw_to_list(@account.favourites.where(yf).joins(:status).group('statuses.account_id').reorder('count_id desc').limit(5).count(:id))
@communi_most = raw_to_list(@account.statuses.where(y).where(y2).joins('join statuses as s2 on statuses.account_id != s2.account_id and (statuses.in_reply_to_id = s2.id or s2.in_reply_to_id = statuses.id)').group('s2.account_id').reorder('count_id desc').limit(5).count(:id))
end
helper_method :display_blocks? helper_method :display_blocks?
helper_method :display_blocks_rationale? helper_method :display_blocks_rationale?
helper_method :public_fetch_mode? helper_method :public_fetch_mode?

+ 2
- 2
app/controllers/api/v1/instances/activity_controller.rb View File

@ -4,7 +4,7 @@ class Api::V1::Instances::ActivityController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_before_action :require_authenticated_user!#, unless: :whitelist_mode?
def show def show
expires_in 1.day, public: true expires_in 1.day, public: true
@ -33,6 +33,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end end
def require_enabled_api! def require_enabled_api!
head 404 unless Setting.activity_api_enabled && !whitelist_mode?
head 404 unless Setting.activity_api_enabled #&& !whitelist_mode?
end end
end end

+ 2
- 2
app/controllers/api/v1/instances/peers_controller.rb View File

@ -4,7 +4,7 @@ class Api::V1::Instances::PeersController < Api::BaseController
before_action :require_enabled_api! before_action :require_enabled_api!
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_before_action :require_authenticated_user!#, unless: :whitelist_mode?
def index def index
expires_in 1.day, public: true expires_in 1.day, public: true
@ -14,6 +14,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
private private
def require_enabled_api! def require_enabled_api!
head 404 unless Setting.peers_api_enabled && !whitelist_mode?
head 404 unless Setting.peers_api_enabled #&& !whitelist_mode?
end end
end end

+ 1
- 1
app/controllers/api/v1/instances_controller.rb View File

@ -2,7 +2,7 @@
class Api::V1::InstancesController < Api::BaseController class Api::V1::InstancesController < Api::BaseController
skip_before_action :set_cache_headers skip_before_action :set_cache_headers
skip_before_action :require_authenticated_user!, unless: :whitelist_mode?
skip_before_action :require_authenticated_user! #, unless: :whitelist_mode?
def show def show
expires_in 3.minutes, public: true expires_in 3.minutes, public: true

+ 17
- 3
app/controllers/api/v1/statuses_controller.rb View File

@ -24,7 +24,11 @@ class Api::V1::StatusesController < Api::BaseController
def context def context
ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account) ancestors_results = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account)
treeId = Rails.configuration.x.tree_address.split('/')[-1].to_i
depth = (@status.id == treeId || (!ancestors_results.empty? && ancestors_results[0].id == treeId)) ? 1 : nil
descendants_results = @status.descendants(CONTEXT_LIMIT, current_account, nil, nil, depth)
loaded_ancestors = cache_collection(ancestors_results, Status) loaded_ancestors = cache_collection(ancestors_results, Status)
loaded_descendants = cache_collection(descendants_results, Status) loaded_descendants = cache_collection(descendants_results, Status)
@ -35,8 +39,13 @@ class Api::V1::StatusesController < Api::BaseController
end end
def create def create
@status = PostStatusService.new.call(current_user.account,
text: status_params[:status],
anon = Rails.configuration.x.anon
anon_name = anon.acc && status_params[:status].strip.end_with?(anon.tag) && generate_anon_name(current_user.account.username + anon.salt + (Time.now - 5.hours).strftime("%D"), anon.namelist, Account.find(anon.acc).note)
sender = anon_name ? Account.find(anon.acc) : current_user.account
st_text = anon_name ? ("[#{anon_name}]:\n#{status_params[:status]}"[0..5000]) : status_params[:status]
@status = PostStatusService.new.call(sender,
text: st_text,
thread: @thread, thread: @thread,
media_ids: status_params[:media_ids], media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive], sensitive: status_params[:sensitive],
@ -98,4 +107,9 @@ class Api::V1::StatusesController < Api::BaseController
def pagination_params(core_params) def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params) params.slice(:limit).permit(:limit).merge(core_params)
end end
def generate_anon_name(k, namelist, note)
name = namelist[Digest::SHA2.hexdigest(k).to_i(16) % namelist.size]
name.in?(note) ? nil : name
end
end end

+ 2
- 1
app/controllers/api/v1/timelines/public_controller.rb View File

@ -37,7 +37,8 @@ class Api::V1::Timelines::PublicController < Api::BaseController
current_account, current_account,
local: truthy_param?(:local), local: truthy_param?(:local),
remote: truthy_param?(:remote), remote: truthy_param?(:remote),
only_media: truthy_param?(:only_media)
only_media: truthy_param?(:only_media),
with_reblogs: true
) )
end end

+ 1
- 1
app/controllers/home_controller.rb View File

@ -41,7 +41,7 @@ class HomeController < ApplicationController
end end
def default_redirect_path def default_redirect_path
if request.path.start_with?('/web') || whitelist_mode?
if request.path.start_with?('/web') # || whitelist_mode?
new_user_session_path new_user_session_path
elsif single_user_mode? elsif single_user_mode?
short_account_path(Account.local.without_suspended.where('id > 0').first) short_account_path(Account.local.without_suspended.where('id > 0').first)

+ 7
- 1
app/helpers/application_helper.rb View File

@ -51,7 +51,8 @@ module ApplicationHelper
def available_sign_up_path def available_sign_up_path
if closed_registrations? if closed_registrations?
'https://joinmastodon.org/#getting-started'
#'https://joinmastodon.org/#getting-started'
'https://closed.social'
else else
new_user_registration_path new_user_registration_path
end end
@ -87,6 +88,11 @@ module ApplicationHelper
policy(record).public_send("#{action}?") policy(record).public_send("#{action}?")
end end
def masked_email(email)
email_username = email.split('@').first
"#{email_username[0]}***#{email_username[-1]}@#{email.split('@').last}"
end
def fa_icon(icon, attributes = {}) def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || [] class_names = attributes[:class]&.split(' ') || []
class_names << 'fa' class_names << 'fa'

+ 55
- 0
app/helpers/context_helper.rb View File

@ -0,0 +1,55 @@
# frozen_string_literal: true
module ContextHelper
NAMED_CONTEXT_MAP = {
activitystreams: 'https://www.w3.org/ns/activitystreams',
security: 'https://w3id.org/security/v1',
}.freeze
CONTEXT_EXTENSION_MAP = {
manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' },
sensitive: { 'sensitive' => 'as:sensitive' },
hashtag: { 'Hashtag' => 'as:Hashtag' },
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze
def full_context
serialized_context(NAMED_CONTEXT_MAP, CONTEXT_EXTENSION_MAP)
end
def serialized_context(named_contexts_map, context_extensions_map)
context_array = []
named_contexts = named_contexts_map.keys
context_extensions = context_extensions_map.keys
named_contexts.each do |key|
context_array << NAMED_CONTEXT_MAP[key]
end
extensions = context_extensions.each_with_object({}) do |key, h|
h.merge!(CONTEXT_EXTENSION_MAP[key])
end
context_array << extensions unless extensions.empty?
if context_array.size == 1
context_array.first
else
context_array
end
end
end

+ 10
- 2
app/helpers/home_helper.rb View File

@ -7,7 +7,7 @@ module HomeHelper
} }
end end
def account_link_to(account, button = '', path: nil)
def account_link_to(account, button = '', path: nil, full: true)
content_tag(:div, class: 'account') do content_tag(:div, class: 'account') do
content_tag(:div, class: 'account__wrapper') do content_tag(:div, class: 'account__wrapper') do
section = if account.nil? section = if account.nil?
@ -20,7 +20,7 @@ module HomeHelper
content_tag(:span, t('about.contact_unavailable'), class: 'display-name__account') content_tag(:span, t('about.contact_unavailable'), class: 'display-name__account')
end end
end end
else
elsif full
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
content_tag(:div, class: 'account__avatar-wrapper') do content_tag(:div, class: 'account__avatar-wrapper') do
image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar') image_tag(full_asset_url(current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url), class: 'account__avatar')
@ -32,6 +32,14 @@ module HomeHelper
content_tag(:span, "@#{account.acct}", class: 'display-name__account') content_tag(:span, "@#{account.acct}", class: 'display-name__account')
end end
end end
else
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') do
content_tag(:span, class: 'display-name') do
content_tag(:bdi) do
content_tag(:strong, display_name(account, custom_emojify: true), class: 'display-name__html emojify')
end
end
end
end end
section + button section + button

+ 80
- 0
app/helpers/jsonld_helper.rb View File

@ -1,6 +1,8 @@
# frozen_string_literal: true # frozen_string_literal: true
module JsonLdHelper module JsonLdHelper
include ContextHelper
def equals_or_includes?(haystack, needle) def equals_or_includes?(haystack, needle)
haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle haystack.is_a?(Array) ? haystack.include?(needle) : haystack == needle
end end
@ -63,6 +65,84 @@ module JsonLdHelper
graph.dump(:normalize) graph.dump(:normalize)
end end
def compact(json)
compacted = JSON::LD::API.compact(json.without('signature'), full_context, documentLoader: method(:load_jsonld_context))
compacted['signature'] = json['signature']
compacted
end
# Patches a JSON-LD document to avoid compatibility issues on redistribution
#
# Since compacting a JSON-LD document against Mastodon's built-in vocabulary
# means other extension namespaces will be expanded, malformed JSON-LD
# attributes lost, and some values “unexpectedly” compacted this method
# patches the following likely sources of incompatibility:
# - 'https://www.w3.org/ns/activitystreams#Public' being compacted to
# 'as:Public' (for instance, pre-3.4.0 Mastodon does not understand
# 'as:Public')
# - single-item arrays being compacted to the item itself (`[foo]` being
# compacted to `foo`)
#
# It is not always possible for `patch_for_forwarding!` to produce a document
# deemed safe for forwarding. Use `safe_for_forwarding?` to check the status
# of the output document.
#
# @param original [Hash] The original JSON-LD document used as reference
# @param compacted [Hash] The compacted JSON-LD document to be patched
# @return [void]
def patch_for_forwarding!(original, compacted)
original.without('@context', 'signature').each do |key, value|
next if value.nil? || !compacted.key?(key)
compacted_value = compacted[key]
if value.is_a?(Hash) && compacted_value.is_a?(Hash)
patch_for_forwarding!(value, compacted_value)
elsif value.is_a?(Array)
compacted_value = [compacted_value] unless compacted_value.is_a?(Array)
return if value.size != compacted_value.size
compacted[key] = value.zip(compacted_value).map do |v, vc|
if v.is_a?(Hash) && vc.is_a?(Hash)
patch_for_forwarding!(v, vc)
vc
elsif v == 'https://www.w3.org/ns/activitystreams#Public' && vc == 'as:Public'
v
else
vc
end
end
elsif value == 'https://www.w3.org/ns/activitystreams#Public' && compacted_value == 'as:Public'
compacted[key] = value
end
end
end
# Tests whether a JSON-LD compaction is deemed safe for redistribution,
# that is, if it doesn't change its meaning to consumers that do not actually
# handle JSON-LD, but rely on values being serialized in a certain way.
#
# See `patch_for_forwarding!` for details.
#
# @param original [Hash] The original JSON-LD document used as reference
# @param compacted [Hash] The compacted JSON-LD document to be patched
# @return [Boolean] Whether the patched document is deemed safe
def safe_for_forwarding?(original, compacted)
original.without('@context', 'signature').all? do |key, value|
compacted_value = compacted[key]
return false unless value.class == compacted_value.class
if value.is_a?(Hash)
safe_for_forwarding?(value, compacted_value)
elsif value.is_a?(Array)
value.zip(compacted_value).all? do |v, vc|
v.is_a?(Hash) ? (vc.is_a?(Hash) && safe_for_forwarding?(v, vc)) : v == vc
end
else
value == compacted_value
end
end
end
def fetch_resource(uri, id, on_behalf_of = nil) def fetch_resource(uri, id, on_behalf_of = nil)
unless id unless id
json = fetch_resource_without_id_validation(uri, on_behalf_of) json = fetch_resource_without_id_validation(uri, on_behalf_of)

+ 1
- 1
app/javascript/images/logo.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M211.80734 139.0875c-3.18125 16.36625-28.4925 34.2775-57.5625 37.74875-15.15875 1.80875-30.08375 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.39125 27.9425 21.11625.7225 39.91875-5.20625 39.91875-5.20625l.8675 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23234 213.82 1.40609 165.31125.20859 116.09125c-.365-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67234 3.45375 78.20359.2425 107.86484 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.975 14.7525 32.975 65.0825 0 0 .41375 37.13375-4.59875 62.915" fill="#3088d4"/><path d="M177.50984 80.077v60.94125h-24.14375v-59.15c0-12.46875-5.24625-18.7975-15.74-18.7975-11.6025 0-17.4175 7.5075-17.4175 22.3525v32.37625H96.20734V85.42325c0-14.845-5.81625-22.3525-17.41875-22.3525-10.49375 0-15.74 6.32875-15.74 18.7975v59.15H38.90484V80.077c0-12.455 3.17125-22.3525 9.54125-29.675 6.56875-7.3225 15.17125-11.07625 25.85-11.07625 12.355 0 21.71125 4.74875 27.8975 14.2475l6.01375 10.08125 6.015-10.08125c6.185-9.49875 15.54125-14.2475 27.8975-14.2475 10.6775 0 19.28 3.75375 25.85 11.07625 6.36875 7.3225 9.54 17.22 9.54 29.675" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57.484 71.644" fill="#2ea7e0"><path d="M0 21.607h7.203v50.416H0z"/><path d="M0 64.82h57.62v7.202H0z"/><path d="M50.417 14.405h7.202V64.82h-7.202z"/><path d="M43.215 0h7.203v14.405h-7.203z"/><path d="M14.405 0h36.013v7.202H14.405zm21.607 42.045c0 1.17-5.74.42-7.202 1.17-4.313 2.217-5.017 7.2-5.017 7.2-2.186 0-2.186 0-2.186-8.37V29.98a1.17 1.17 0 0 1 1.17-1.17h12.065a1.17 1.17 0 0 1 1.17 1.17v12.066z"/><path d="M7.203 0h7.202v14.405H7.203z"/></svg>

+ 1
- 1
app/javascript/images/logo_alt.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.41507 232.00976"><path d="M211.80683 139.0875c-3.1825 16.36625-28.4925 34.2775-57.5625 37.74875-15.16 1.80875-30.0825 3.47125-45.99875 2.74125-26.0275-1.1925-46.565-6.2125-46.565-6.2125 0 2.53375.15625 4.94625.46875 7.2025 3.38375 25.68625 25.47 27.225 46.3925 27.9425 21.115.7225 39.91625-5.20625 39.91625-5.20625l.86875 19.09s-14.77 7.93125-41.08125 9.39c-14.50875.7975-32.52375-.365-53.50625-5.91875C9.23183 213.82 1.40558 165.31125.20808 116.09125c-.36375-14.61375-.14-28.39375-.14-39.91875 0-50.33 32.97625-65.0825 32.97625-65.0825C49.67058 3.45375 78.20308.2425 107.86433 0h.72875c29.66125.2425 58.21125 3.45375 74.8375 11.09 0 0 32.97625 14.7525 32.97625 65.0825 0 0 .4125 37.13375-4.6 62.915" fill="#3088d4"/><path d="M65.68743 96.45938c0 9.01375-7.3075 16.32125-16.3225 16.32125-9.01375 0-16.32-7.3075-16.32-16.32125 0-9.01375 7.30625-16.3225 16.32-16.3225 9.015 0 16.3225 7.30875 16.3225 16.3225M124.52893 96.45938c0 9.01375-7.30875 16.32125-16.3225 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.3225 7.30875 16.3225 16.3225M183.36933 96.45938c0 9.01375-7.3075 16.32125-16.32125 16.32125-9.01375 0-16.32125-7.3075-16.32125-16.32125 0-9.01375 7.3075-16.3225 16.32125-16.3225 9.01375 0 16.32125 7.30875 16.32125 16.3225" fill="#fff"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57.484 71.644" fill="#2ea7e0"><path d="M0 21.607h7.203v50.416H0z"/><path d="M0 64.82h57.62v7.202H0z"/><path d="M50.417 14.405h7.202V64.82h-7.202z"/><path d="M43.215 0h7.203v14.405h-7.203z"/><path d="M14.405 0h36.013v7.202H14.405zm21.607 42.045c0 1.17-5.74.42-7.202 1.17-4.313 2.217-5.017 7.2-5.017 7.2-2.186 0-2.186 0-2.186-8.37V29.98a1.17 1.17 0 0 1 1.17-1.17h12.065a1.17 1.17 0 0 1 1.17 1.17v12.066z"/><path d="M7.203 0h7.202v14.405H7.203z"/></svg>

+ 5
- 1
app/javascript/images/logo_full.svg
File diff suppressed because it is too large
View File


+ 1
- 1
app/javascript/images/logo_transparent.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" /></symbol></svg>
<svg xmlns="http://www.w3.org/2000/svg"><symbol id="mastodon-svg-logo" viewBox="0 0 57.484 71.644"><path d="M0 21.607h7.203v50.416H0z"/><path d="M0 64.82h57.62v7.202H0z"/><path d="M50.417 14.405h7.202V64.82h-7.202z"/><path d="M43.215 0h7.203v14.405h-7.203z"/><path d="M14.405 0h36.013v7.202H14.405zm21.607 42.045c0 1.17-5.74.42-7.202 1.17-4.313 2.217-5.017 7.2-5.017 7.2-2.186 0-2.186 0-2.186-8.37V29.98a1.17 1.17 0 0 1 1.17-1.17h12.065a1.17 1.17 0 0 1 1.17 1.17v12.066z"/><path d="M7.203 0h7.202v14.405H7.203z"/></symbol></svg>

+ 1
- 1
app/javascript/images/logo_transparent_black.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 216.4144 232.00976"><path d="M107.86523 0C78.203984.2425 49.672422 3.4535937 33.044922 11.089844c0 0-32.97656262 14.752031-32.97656262 65.082031 0 11.525-.224375 25.306175.140625 39.919925 1.19750002 49.22 9.02375002 97.72843 54.53124962 109.77343 20.9825 5.55375 38.99711 6.71547 53.505856 5.91797 26.31125-1.45875 41.08203-9.38867 41.08203-9.38867l-.86914-19.08984s-18.80171 5.92758-39.91796 5.20508c-20.921254-.7175-43.006879-2.25516-46.390629-27.94141-.3125-2.25625-.46875-4.66938-.46875-7.20313 0 0 20.536953 5.0204 46.564449 6.21289 15.915.73001 30.8393-.93343 45.99805-2.74218 29.07-3.47125 54.38125-21.3818 57.5625-37.74805 5.0125-25.78125 4.59961-62.916015 4.59961-62.916015 0-50.33-32.97461-65.082031-32.97461-65.082031C166.80539 3.4535938 138.255.2425 108.59375 0h-.72852zM74.296875 39.326172c12.355 0 21.710234 4.749297 27.896485 14.248047l6.01367 10.080078 6.01563-10.080078c6.185-9.49875 15.54023-14.248047 27.89648-14.248047 10.6775 0 19.28156 3.753672 25.85156 11.076172 6.36875 7.3225 9.53907 17.218828 9.53907 29.673828v60.941408h-24.14454V81.869141c0-12.46875-5.24453-18.798829-15.73828-18.798829-11.6025 0-17.41797 7.508516-17.41797 22.353516v32.375002H96.207031V85.423828c0-14.845-5.815468-22.353515-17.417969-22.353516-10.49375 0-15.740234 6.330079-15.740234 18.798829v59.148439H38.904297V80.076172c0-12.455 3.171016-22.351328 9.541015-29.673828 6.568751-7.3225 15.172813-11.076172 25.851563-11.076172z" fill="#000"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 57.484 71.644" fill="#000"><path d="M0 21.607h7.203v50.416H0z"/><path d="M0 64.82h57.62v7.202H0z"/><path d="M50.417 14.405h7.202V64.82h-7.202z"/><path d="M43.215 0h7.203v14.405h-7.203z"/><path d="M14.405 0h36.013v7.202H14.405zm21.607 42.045c0 1.17-5.74.42-7.202 1.17-4.313 2.217-5.017 7.2-5.017 7.2-2.186 0-2.186 0-2.186-8.37V29.98a1.17 1.17 0 0 1 1.17-1.17h12.065a1.17 1.17 0 0 1 1.17 1.17v12.066z"/><path d="M7.203 0h7.202v14.405H7.203z"/></svg>

BIN
View File


BIN
View File


+ 8
- 2
app/javascript/mastodon/actions/interactions.js View File

@ -60,8 +60,11 @@ export function unreblog(status) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(unreblogRequest(status)); dispatch(unreblogRequest(status));
let old_reblogs_count = status.get('reblogs_count');
api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => { api(getState).post(`/api/v1/statuses/${status.get('id')}/unreblog`).then(response => {
dispatch(importFetchedStatus(response.data));
let pred = response.data
pred.reblogs_count = old_reblogs_count - 1; // unreb is async
dispatch(importFetchedStatus(pred));
dispatch(unreblogSuccess(status)); dispatch(unreblogSuccess(status));
}).catch(error => { }).catch(error => {
dispatch(unreblogFail(status, error)); dispatch(unreblogFail(status, error));
@ -136,8 +139,11 @@ export function unfavourite(status) {
return (dispatch, getState) => { return (dispatch, getState) => {
dispatch(unfavouriteRequest(status)); dispatch(unfavouriteRequest(status));
let old_favourites_count = status.get('favourites_count');
api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => { api(getState).post(`/api/v1/statuses/${status.get('id')}/unfavourite`).then(response => {
dispatch(importFetchedStatus(response.data));
let pred = response.data
pred.favourites_count = old_favourites_count - 1; // unfav is async
dispatch(importFetchedStatus(pred));
dispatch(unfavouriteSuccess(status)); dispatch(unfavouriteSuccess(status));
}).catch(error => { }).catch(error => {
dispatch(unfavouriteFail(status, error)); dispatch(unfavouriteFail(status, error));

+ 4
- 1
app/javascript/mastodon/actions/timelines.js View File

@ -4,7 +4,7 @@ import api, { getLinks } from 'mastodon/api';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
import compareId from 'mastodon/compare_id'; import compareId from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state'; import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { fetchContext } from './statuses';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE'; export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE'; export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR'; export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
@ -112,6 +112,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
const next = getLinks(response).refs.find(link => link.rel === 'next'); const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data)); dispatch(importFetchedStatuses(response.data));
dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems)); dispatch(expandTimelineSuccess(timelineId, response.data, next ? next.uri : null, response.status === 206, isLoadingRecent, isLoadingMore, isLoadingRecent && preferPendingItems));
response.data.forEach(status => {
dispatch(fetchContext(status.reblog ? status.reblog.id : status.id));
});
if (timelineId === 'home') { if (timelineId === 'home') {
dispatch(submitMarkers()); dispatch(submitMarkers());

+ 12
- 1
app/javascript/mastodon/components/media_gallery.js View File

@ -161,7 +161,18 @@ class Item extends React.PureComponent {
const x = ((focusX / 2) + .5) * 100; const x = ((focusX / 2) + .5) * 100;
const y = ((focusY / -2) + .5) * 100; const y = ((focusY / -2) + .5) * 100;
thumbnail = (
const descrip = attachment.get('description');
thumbnail = (descrip && descrip.startsWith('https://'))?
(
<iframe
src={descrip}
width='100%'
height='100%'
onLoad={this.handleImageLoad}
></iframe>
)
:
(
<a <a
className='media-gallery__item-thumbnail' className='media-gallery__item-thumbnail'
href={attachment.get('remote_url') || originalUrl} href={attachment.get('remote_url') || originalUrl}

+ 80
- 4
app/javascript/mastodon/components/status.js View File

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Avatar from './avatar'; import Avatar from './avatar';
@ -10,7 +11,7 @@ import StatusContent from './status_content';
import StatusActionBar from './status_action_bar'; import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list'; import AttachmentList from './attachment_list';
import Card from '../features/status/components/card'; import Card from '../features/status/components/card';
import { injectIntl, defineMessages, FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages, FormattedMessage, FormattedNumber } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
@ -19,6 +20,8 @@ import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state'; import { displayMedia } from '../initial_state';
import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder'; import PictureInPicturePlaceholder from 'mastodon/components/picture_in_picture_placeholder';
import StatusContainer from '../containers/status_container2';
// We use the component (and not the container) since we do not want // We use the component (and not the container) since we do not want
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
@ -101,6 +104,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool, inUse: PropTypes.bool,
available: PropTypes.bool, available: PropTypes.bool,
}), }),
sonsIds: ImmutablePropTypes.list,
}; };
// Avoid checking props that are functions (and whose equality will always // Avoid checking props that are functions (and whose equality will always
@ -112,6 +116,8 @@ class Status extends ImmutablePureComponent {
'hidden', 'hidden',
'unread', 'unread',
'pictureInPicture', 'pictureInPicture',
'sonsIds',
'ancestorsText',
]; ];
state = { state = {
@ -279,11 +285,40 @@ class Status extends ImmutablePureComponent {
this.node = c; this.node = c;
} }
renderChildren (list) {
return list.map(e => (
e.id ?
<div key={`comments-1-${e.id}`}>
<StatusContainer
key={e.id}
id={e.id}
onMoveUp={()=>{}}
onMoveDown={()=>{}}
contextType='comments-timeline'
/>
{ e.sonsIds &&
<div key={`comments-2-${e.id}`} className='comments-timeline-2'>{this.renderChildren(e.sonsIds)}</div>
}
</div>
:
<StatusContainer
key={e}
id={e}
onMoveUp={()=>{}}
onMoveDown={()=>{}}
contextType='comments-timeline'
/>
));
}
render () { render () {
let media = null; let media = null;
let statusAvatar, prepend, rebloggedByText; let statusAvatar, prepend, rebloggedByText;
let sons, quote;
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, pictureInPicture } = this.props;
const { intl, hidden, featured, otherAccounts, unread, showThread, scrollKey, pictureInPicture, deep, tree_type, ancestorsText, sonsIds } = this.props;
let { status, account, ...other } = this.props; let { status, account, ...other } = this.props;
@ -459,9 +494,44 @@ class Status extends ImmutablePureComponent {
const visibilityIcon = visibilityIconInfo[status.get('visibility')]; const visibilityIcon = visibilityIconInfo[status.get('visibility')];
if (sonsIds && sonsIds.size > 0) {
sons = <div className='comments-timeline__wrapper'><div className='comments-timeline'>{this.renderChildren(sonsIds)}</div></div>;
}
if (rebloggedByText && status.get('in_reply_to_id')) {
quote = ancestorsText ?
<div className='status__tree__quote__wrapper' onClick={this.handleClick}>
<Icon id="tree" />
{ancestorsText}
</div>
:
<div className='status__quote__wrapper'>
<StatusContainer
key={status.get('in_reply_to_id')}
id={status.get('in_reply_to_id')}
onMoveUp={()=>{}}
onMoveDown={()=>{}}
contextType='quote'
/>
</div>;
}
let deepRec;
if(deep != null) {
deepRec = (
<div className="detailed-status__button deep__number">
<Icon id="tree" />
<span>
<FormattedNumber value={deep} />
</span>
</div>
);
}
return ( return (
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), unread, focusable: !this.props.muted }, (deep!=null) && 'tree-'+tree_type)} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend} {prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}> <div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
@ -481,13 +551,19 @@ class Status extends ImmutablePureComponent {
</a> </a>
</div> </div>
{deepRec}
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} /> <StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
{media} {media}
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
{quote}
{(deep == null || tree_type != 'ance') && (
<StatusActionBar scrollKey={scrollKey} status={status} account={account} {...other} />
)}
</div> </div>
</div> </div>
{sons}
</HotKeys> </HotKeys>
); );
} }

+ 473
- 0
app/javascript/mastodon/components/status2.js View File

@ -0,0 +1,473 @@
import React from 'react';
import Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import AvatarComposite from './avatar_composite';
import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
import StatusActionBar from './status_action_bar';
import AttachmentList from './attachment_list';
import Card from '../features/status/components/card';
import { injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { HotKeys } from 'react-hotkeys';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
//import DetailedStatus from '../features/status/components/detailed_status';
// We use the component (and not the container) since we do not want
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
export const textForScreenReader = (intl, status, rebloggedByText = false) => {
const displayName = status.getIn(['account', 'display_name']);
const values = [
displayName.length === 0 ? status.getIn(['account', 'acct']).split('@')[0] : displayName,
status.get('spoiler_text') && status.get('hidden') ? status.get('spoiler_text') : status.get('search_index').slice(status.get('spoiler_text').length),
intl.formatDate(status.get('created_at'), { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' }),
status.getIn(['account', 'acct']),
];
if (rebloggedByText) {
values.push(rebloggedByText);
}
return values.join(', ');
};
export const defaultMediaVisibility = (status) => {
if (!status) {
return undefined;
}
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
status = status.get('reblog');
}
return (displayMedia !== 'hide_all' && !status.get('sensitive') || displayMedia === 'show_all');
};
export default @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onPin: PropTypes.func,
onOpenMedia: PropTypes.func,
onOpenVideo: PropTypes.func,
onBlock: PropTypes.func,
onEmbed: PropTypes.func,
onHeightChange: PropTypes.func,
onToggleHidden: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
showThread: PropTypes.bool,
getScrollPosition: PropTypes.func,
updateScrollBottom: PropTypes.func,
cacheMediaWidth: PropTypes.func,
cachedMediaWidth: PropTypes.number,
sonsIds: ImmutablePropTypes.list,
};
// Avoid checking props that are functions (and whose equality will always
// evaluate to false. See react-immutable-pure-component for usage.
updateOnProps = [
'status',
'account',
'muted',
'hidden',
];
state = {
showMedia: defaultMediaVisibility(this.props.status),
statusId: undefined,
};
// Track height changes we know about to compensate scrolling
componentDidMount () {
this.didShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
}
getSnapshotBeforeUpdate () {
if (this.props.getScrollPosition) {
return this.props.getScrollPosition();
} else {
return null;
}
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) {
return {
showMedia: defaultMediaVisibility(nextProps.status),
statusId: nextProps.status.get('id'),
};
} else {
return null;
}
}
// Compensate height changes
componentDidUpdate (prevProps, prevState, snapshot) {
const doShowCard = !this.props.muted && !this.props.hidden && this.props.status && this.props.status.get('card');
if (doShowCard && !this.didShowCard) {
this.didShowCard = true;
if (snapshot !== null && this.props.updateScrollBottom) {
if (this.node && this.node.offsetTop < snapshot.top) {
this.props.updateScrollBottom(snapshot.height - snapshot.top);
}
}
}
}
componentWillUnmount() {
if (this.node && this.props.getScrollPosition) {
const position = this.props.getScrollPosition();
if (position !== null && this.node.offsetTop < position.top) {
requestAnimationFrame(() => {
this.props.updateScrollBottom(position.height - position.top);
});
}
}
}
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
handleExpandClick = (e) => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (e.button === 0) {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${status.getIn(['reblog', 'id'], status.get('id'))}`);
}
}
handleAccountClick = (e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
const id = e.currentTarget.getAttribute('data-id');
e.preventDefault();
this.context.router.history.push(`/accounts/${id}`);
}
}
handleExpandedToggle = () => {
this.props.onToggleHidden(this._properStatus());
};
renderLoadingMediaGallery () {
return <div className='media-gallery' style={{ height: '110px' }} />;
}
renderLoadingVideoPlayer () {
return <div className='video-player' style={{ height: '110px' }} />;
}
renderLoadingAudioPlayer () {
return <div className='audio-player' style={{ height: '110px' }} />;
}
handleOpenVideo = (media, startTime) => {
this.props.onOpenVideo(media, startTime);
}
handleHotkeyReply = e => {
e.preventDefault();
this.props.onReply(this._properStatus(), this.context.router.history);
}
handleHotkeyFavourite = () => {
this.props.onFavourite(this._properStatus());
}
handleHotkeyBoost = e => {
this.props.onReblog(this._properStatus(), e);
}
handleHotkeyMention = e => {
e.preventDefault();
this.props.onMention(this._properStatus().get('account'), this.context.router.history);
}
handleHotkeyOpen = () => {
this.context.router.history.push(`/statuses/${this._properStatus().get('id')}`);
}
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/accounts/${this._properStatus().getIn(['account', 'id'])}`);
}
handleHotkeyMoveUp = e => {
this.props.onMoveUp(this.props.status.get('id'), e.target.getAttribute('data-featured'));
}
handleHotkeyMoveDown = e => {
this.props.onMoveDown(this.props.status.get('id'), e.target.getAttribute('data-featured'));
}
handleHotkeyToggleHidden = () => {
this.props.onToggleHidden(this._properStatus());
}
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
_properStatus () {
const { status } = this.props;
if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
return status.get('reblog');
} else {
return status;
}
}
handleRef = c => {
this.node = c;
}
render () {
let media = null;
let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured, otherAccounts, unread, showThread, sonsIds } = this.props;
let { status, account, ...other } = this.props;
if (status === null) {
return null;
}
const handlers = this.props.muted ? {} : {
reply: this.handleHotkeyReply,
favourite: this.handleHotkeyFavourite,
boost: this.handleHotkeyBoost,
mention: this.handleHotkeyMention,
open: this.handleHotkeyOpen,
openProfile: this.handleHotkeyOpenProfile,
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
toggleHidden: this.handleHotkeyToggleHidden,
toggleSensitive: this.handleHotkeyToggleSensitive,
};
if (hidden) {
return (
<HotKeys handlers={handlers}>
<div ref={this.handleRef} className={classNames('status__wrapper', { focusable: !this.props.muted })} tabIndex='0'>
{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}
{status.get('content')}
</div>
</HotKeys>
);
}
if (status.get('filtered') || status.getIn(['reblog', 'filtered'])) {
const minHandlers = this.props.muted ? {} : {
moveUp: this.handleHotkeyMoveUp,
moveDown: this.handleHotkeyMoveDown,
};
return (
<HotKeys handlers={minHandlers}>
<div className='status__wrapper status__wrapper--filtered focusable' tabIndex='0' ref={this.handleRef}>
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
</div>
</HotKeys>
);
}
if (featured) {
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='thumb-tack' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.pinned' defaultMessage='Pinned toot' />
</div>
);
} else if (status.get('reblog', null) !== null && typeof status.get('reblog') === 'object') {
const display_name_html = { __html: status.getIn(['account', 'display_name_html']) };
prepend = (
<div className='status__prepend'>
<div className='status__prepend-icon-wrapper'><Icon id='retweet' className='status__prepend-icon' fixedWidth /></div>
<FormattedMessage id='status.reblogged_by' defaultMessage='{name} boosted' values={{ name: <a onClick={this.handleAccountClick} data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} className='status__display-name muted'><bdi><strong dangerouslySetInnerHTML={display_name_html} /></bdi></a> }} />
</div>
);
rebloggedByText = intl.formatMessage({ id: 'status.reblogged_by', defaultMessage: '{name} boosted' }, { name: status.getIn(['account', 'acct']) });
account = status.get('account');
status = status.get('reblog');
}
if (status.get('media_attachments').size > 0) {
if (this.props.muted) {
media = (
<AttachmentList
compact
media={status.get('media_attachments')}
/>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Audio} loading={this.renderLoadingAudioPlayer} >
{Component => (
<Component
src={attachment.get('url')}
alt={attachment.get('description')}
duration={attachment.getIn(['meta', 'original', 'duration'], 0)}
peaks={[0]}
height={70}
/>
)}
</Bundle>
);
} else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
<Bundle fetchComponent={Video} loading={this.renderLoadingVideoPlayer} >
{Component => (
<Component
preview={attachment.get('preview_url')}
blurhash={attachment.get('blurhash')}
src={attachment.get('url')}
alt={attachment.get('description')}
width={this.props.cachedMediaWidth}
height={110}
inline
sensitive={status.get('sensitive')}
onOpenVideo={this.handleOpenVideo}
cacheWidth={this.props.cacheMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
} else {
media = (
<Bundle fetchComponent={MediaGallery} loading={this.renderLoadingMediaGallery}>
{Component => (
<Component
media={status.get('media_attachments')}
sensitive={status.get('sensitive')}
height={110}
onOpenMedia={this.props.onOpenMedia}
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
visible={this.state.showMedia}
onToggleVisibility={this.handleToggleMediaVisibility}
/>
)}
</Bundle>
);
}
} else if (status.get('spoiler_text').length === 0 && status.get('card')) {
media = (
<Card
onOpenMedia={this.props.onOpenMedia}
card={status.get('card')}
compact
cacheWidth={this.props.cacheMediaWidth}
defaultWidth={this.props.cachedMediaWidth}
/>
);
}
if (otherAccounts && otherAccounts.size > 0) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
} else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />;
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText)} ref={this.handleRef}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__expand' onClick={this.handleExpandClick} role='presentation' />
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
<a onClick={this.handleAccountClick} target='_blank' data-id={status.getIn(['account', 'id'])} href={status.getIn(['account', 'url'])} title={status.getIn(['account', 'acct'])} className='status__display-name'>
<div className='status__avatar'>
{statusAvatar}
</div>
<DisplayName account={status.get('account')} others={otherAccounts} />
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
{media}
{showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) && (
<button className='status__content__read-more-button' onClick={this.handleClick}>
<FormattedMessage id='status.show_thread' defaultMessage='Show thread' />
</button>
)}
<StatusActionBar status={status} account={account} {...other} />
</div>
</div>
</HotKeys>
);
}
}

+ 7
- 6
app/javascript/mastodon/components/status_action_bar.js View File

@ -17,6 +17,7 @@ const messages = defineMessages({
mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' }, block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
share: { id: 'status.share', defaultMessage: 'Share' }, share: { id: 'status.share', defaultMessage: 'Share' },
more: { id: 'status.more', defaultMessage: 'More' }, more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
@ -297,11 +298,11 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon; let replyIcon;
let replyTitle; let replyTitle;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else {
replyIcon = 'reply'; replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply); replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
} }
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
@ -323,9 +324,9 @@ class StatusActionBar extends ImmutablePureComponent {
return ( return (
<div className='status__action-bar'> <div className='status__action-bar'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} />
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'comment-o' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='heart' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
{shareButton} {shareButton}

+ 1
- 1
app/javascript/mastodon/components/status_content.js View File

@ -172,7 +172,7 @@ export default class StatusContent extends React.PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden; const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
const renderReadMore = this.props.onClick && status.get('collapsed'); const renderReadMore = this.props.onClick && status.get('collapsed');
const renderViewThread = this.props.showThread && status.get('in_reply_to_id') && status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
const renderViewThread = this.props.showThread && status.get('in_reply_to_id') //&& status.get('in_reply_to_account_id') === status.getIn(['account', 'id']);
const content = { __html: status.get('contentHtml') }; const content = { __html: status.get('contentHtml') };
const spoilerContent = { __html: status.get('spoilerHtml') }; const spoilerContent = { __html: status.get('spoilerHtml') };

+ 64
- 4
app/javascript/mastodon/containers/status_container.js View File

@ -1,5 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Immutable from 'immutable';
import { createSelector } from 'reselect';
import Status from '../components/status'; import Status from '../components/status';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors'; import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import { import {
@ -40,7 +42,7 @@ import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal'; import { openModal } from '../actions/modal';
import { deployPictureInPicture } from '../actions/picture_in_picture'; import { deployPictureInPicture } from '../actions/picture_in_picture';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
import { boostModal, deleteModal, treeRoot } from '../initial_state';
import { showAlertForError } from '../actions/alerts'; import { showAlertForError } from '../actions/alerts';
const messages = defineMessages({ const messages = defineMessages({
@ -57,11 +59,69 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus(); const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture(); const getPictureInPicture = makeGetPictureInPicture();
const mapStateToProps = (state, props) => ({
status: getStatus(state, props),
pictureInPicture: getPictureInPicture(state, props),
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
}); });
const getAncestorsText = createSelector([
(_, {ids}) => ids,
state => state.get('statuses'),
], (ids, statuses) => '>> '+ids.map(i => {
let text = statuses.get(i) ? statuses.get(i).get('search_index') : i;
if(text.length > 16)
text = text.slice(0,13) + "...";
return text;
}).join(' >> ')
);
const getSonsIds = createSelector([
(_, {id}) => id,
state => state.getIn(['contexts', 'replies']),
], (statusId, contextReplies) => {
const sons = contextReplies.get(statusId);
return sons ? sons.map(id => ({
'id': id,
'sonsIds' : contextReplies.get(id),
}))
: null;
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, props);
let ancestorsIds = Immutable.List();
let ancestorsText;
let sonsIds;
if (props.showThread && status) {
sonsIds = getSonsIds(state, { id : status.getIn(['reblog', 'id'], props.id)});
if(status.get('reblog')) {
ancestorsIds = getAncestorsIds(state, { id: status.getIn(['reblog', 'in_reply_to_id']) });
if(ancestorsIds && treeRoot && ancestorsIds.first() == treeRoot.split('/').pop()) {
ancestorsText = getAncestorsText(state, { ids: ancestorsIds.shift() });
}
}
}
return {
status,
ancestorsText,
sonsIds,
pictureInPicture: getPictureInPicture(state, props),
};
};
return mapStateToProps; return mapStateToProps;
}; };

+ 168
- 0
app/javascript/mastodon/containers/status_container2.js View File

@ -0,0 +1,168 @@
import { connect } from 'react-redux';
import Status from '../components/status2';
import { makeGetStatus } from '../selectors';
import {
replyCompose,
mentionCompose,
directCompose,
} from '../actions/compose';
import {
reblog,
favourite,
unreblog,
unfavourite,
pin,
unpin,
} from '../actions/interactions';
import {
muteStatus,
unmuteStatus,
deleteStatus,
hideStatus,
revealStatus,
} from '../actions/statuses';
import { initMuteModal } from '../actions/mutes';
import { initBlockModal } from '../actions/blocks';
import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { defineMessages, injectIntl } from 'react-intl';
import { boostModal, deleteModal } from '../initial_state';
import { showAlertForError } from '../actions/alerts';
const messages = defineMessages({
deleteConfirm: { id: 'confirmations.delete.confirm', defaultMessage: 'Delete' },
deleteMessage: { id: 'confirmations.delete.message', defaultMessage: 'Are you sure you want to delete this status?' },
redraftConfirm: { id: 'confirmations.redraft.confirm', defaultMessage: 'Delete & redraft' },
redraftMessage: { id: 'confirmations.redraft.message', defaultMessage: 'Are you sure you want to delete this status and re-draft it? Favourites and boosts will be lost, and replies to the original post will be orphaned.' },
replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' },
replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' },
});
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const mapStateToProps = (state, props) => ({
status: getStatus(state, props),
sonsIds: state.getIn(['contexts', 'replies', props.id]),
});
return mapStateToProps;
};
const mapDispatchToProps = (dispatch, { intl }) => ({
onReply (status, router) {
dispatch((_, getState) => {
let state = getState();
if (state.getIn(['compose', 'text']).trim().length !== 0) {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.replyMessage),
confirm: intl.formatMessage(messages.replyConfirm),
onConfirm: () => dispatch(replyCompose(status, router)),
}));
} else {
dispatch(replyCompose(status, router));
}
});
},
onModalReblog (status) {
if (status.get('reblogged')) {
dispatch(unreblog(status));
} else {
dispatch(reblog(status));
}
},
onReblog (status, e) {
if ((e && e.shiftKey) || !boostModal) {
this.onModalReblog(status);
} else {
dispatch(openModal('BOOST', { status, onReblog: this.onModalReblog }));
}
},
onFavourite (status) {
if (status.get('favourited')) {
dispatch(unfavourite(status));
} else {
dispatch(favourite(status));
}
},
onPin (status) {
if (status.get('pinned')) {
dispatch(unpin(status));
} else {
dispatch(pin(status));
}
},
onEmbed (status) {
dispatch(openModal('EMBED', {
url: status.get('url'),
onError: error => dispatch(showAlertForError(error)),
}));
},
onDelete (status, history, withRedraft = false) {
if (!deleteModal) {
dispatch(deleteStatus(status.get('id'), history, withRedraft));
} else {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(withRedraft ? messages.redraftMessage : messages.deleteMessage),
confirm: intl.formatMessage(withRedraft ? messages.redraftConfirm : messages.deleteConfirm),
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
}));
}
},
onDirect (account, router) {
dispatch(directCompose(account, router));
},
onMention (account, router) {
dispatch(mentionCompose(account, router));
},
onOpenMedia (media, index) {
dispatch(openModal('MEDIA', { media, index }));
},
onOpenVideo (media, time) {
dispatch(openModal('VIDEO', { media, time }));
},
onBlock (status) {
const account = status.get('account');
dispatch(initBlockModal(account));
},
onReport (status) {
dispatch(initReport(status.get('account'), status));
},
onMute (account) {
dispatch(initMuteModal(account));
},
onMuteConversation (status) {
if (status.get('muted')) {
dispatch(unmuteStatus(status.get('id')));
} else {
dispatch(muteStatus(status.get('id')));
}
},
onToggleHidden (status) {
if (status.get('hidden')) {
dispatch(revealStatus(status.get('id')));
} else {
dispatch(hideStatus(status.get('id')));
}
},
});
export default injectIntl(connect(makeMapStateToProps, mapDispatchToProps)(Status));

+ 2
- 2
app/javascript/mastodon/features/compose/components/compose_form.js View File

@ -86,7 +86,7 @@ class ComposeForm extends ImmutablePureComponent {
const fulltext = this.getFulltextForCharacterCounting(); const fulltext = this.getFulltextForCharacterCounting();
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0; const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 500 || (isOnlyWhitespace && !anyMedia));
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 5000 || (isOnlyWhitespace && !anyMedia));
} }
handleSubmit = () => { handleSubmit = () => {
@ -257,7 +257,7 @@ class ComposeForm extends ImmutablePureComponent {
<PrivacyDropdownContainer /> <PrivacyDropdownContainer />
<SpoilerButtonContainer /> <SpoilerButtonContainer />
</div> </div>
<div className='character-counter__wrapper'><CharacterCounter max={500} text={this.getFulltextForCharacterCounting()} /></div>
<div className='character-counter__wrapper'><CharacterCounter max={5000} text={this.getFulltextForCharacterCounting()} /></div>
</div> </div>
<div className='compose-form__publish'> <div className='compose-form__publish'>

+ 1
- 1
app/javascript/mastodon/features/compose/components/poll_form.js View File

@ -157,7 +157,7 @@ class PollForm extends ImmutablePureComponent {
</ul> </ul>
<div className='poll__footer'> <div className='poll__footer'>
<button disabled={options.size >= 4} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
<button disabled={options.size >= 10} className='button button-secondary' onClick={this.handleAddOption}><Icon id='plus' /> <FormattedMessage {...messages.add_option} /></button>
{/* eslint-disable-next-line jsx-a11y/no-onchange */} {/* eslint-disable-next-line jsx-a11y/no-onchange */}
<select value={expiresIn} onChange={this.handleSelectDuration}> <select value={expiresIn} onChange={this.handleSelectDuration}>

+ 16
- 1
app/javascript/mastodon/features/compose/index.js View File

@ -14,13 +14,16 @@ import SearchResultsContainer from './containers/search_results_container';
import { changeComposing } from '../../actions/compose'; import { changeComposing } from '../../actions/compose';
import { openModal } from 'mastodon/actions/modal'; import { openModal } from 'mastodon/actions/modal';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg'; import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state';
import { mascot, treeRoot } from '../../initial_state';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out'; import { logOut } from 'mastodon/utils/log_out';
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
const messages = defineMessages({ const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' }, start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' }, home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
tree: {id: 'tabs_bar.tree', defaultMessage: 'Tree'},
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' }, notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' }, public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' }, community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
@ -49,6 +52,10 @@ class Compose extends React.PureComponent {
intl: PropTypes.object.isRequired, intl: PropTypes.object.isRequired,
}; };
state = {
showPinned: true,
}
componentDidMount () { componentDidMount () {
const { isSearchPage } = this.props; const { isSearchPage } = this.props;
@ -88,9 +95,14 @@ class Compose extends React.PureComponent {
onBlur = () => { onBlur = () => {
this.props.dispatch(changeComposing(false)); this.props.dispatch(changeComposing(false));
} }
handleClear = () => {
this.setState({ showPinned: false});
}
render () { render () {
const { multiColumn, showSearch, isSearchPage, intl } = this.props; const { multiColumn, showSearch, isSearchPage, intl } = this.props;
const { showPinned } = this.state;
let header = ''; let header = '';
@ -102,6 +114,9 @@ class Compose extends React.PureComponent {
{!columns.some(column => column.get('id') === 'HOME') && ( {!columns.some(column => column.get('id') === 'HOME') && (
<Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link> <Link to='/timelines/home' className='drawer__tab' title={intl.formatMessage(messages.home_timeline)} aria-label={intl.formatMessage(messages.home_timeline)}><Icon id='home' fixedWidth /></Link>
)} )}
{!columns.some(column => column.get('id') === 'TREE') && treeRoot && (
<Link to={treeRoot} className='drawer__tab' title={intl.formatMessage(messages.tree)} aria-label={intl.formatMessage(messages.tree)}><Icon id='tree' fixedWidth /></Link>
)}
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && ( {!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link> <Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
)} )}

+ 1
- 1
app/javascript/mastodon/features/favourited_statuses/index.js View File

@ -75,7 +75,7 @@ class Favourites extends ImmutablePureComponent {
return ( return (
<Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}> <Column bindToDocument={!multiColumn} ref={this.setRef} label={intl.formatMessage(messages.heading)}>
<ColumnHeader <ColumnHeader
icon='star'
icon='heart'
title={intl.formatMessage(messages.heading)} title={intl.formatMessage(messages.heading)}
onPin={this.handlePin} onPin={this.handlePin}
onMove={this.handleMove} onMove={this.handleMove}

+ 1
- 1
app/javascript/mastodon/features/getting_started/components/trends.js View File

@ -38,7 +38,7 @@ export default class Trends extends ImmutablePureComponent {
<div className='getting-started__trends'> <div className='getting-started__trends'>
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4> <h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
{trends.take(5).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div> </div>
); );
} }

+ 1
- 1
app/javascript/mastodon/features/getting_started/index.js View File

@ -135,7 +135,7 @@ class GettingStarted extends ImmutablePureComponent {
navItems.push( navItems.push(
<ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />, <ColumnLink key='direct' icon='envelope' text={intl.formatMessage(messages.direct)} to='/timelines/direct' />,
<ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />, <ColumnLink key='bookmark' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} to='/bookmarks' />,
<ColumnLink key='favourites' icon='star' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='favourites' icon='heart' text={intl.formatMessage(messages.favourites)} to='/favourites' />,
<ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />, <ColumnLink key='lists' icon='list-ul' text={intl.formatMessage(messages.lists)} to='/lists' />,
); );

+ 2
- 2
app/javascript/mastodon/features/notifications/components/filter_bar.js View File

@ -65,14 +65,14 @@ class FilterBar extends React.PureComponent {
onClick={this.onClick('mention')} onClick={this.onClick('mention')}
title={intl.formatMessage(tooltips.mentions)} title={intl.formatMessage(tooltips.mentions)}
> >
<Icon id='reply-all' fixedWidth />
<Icon id='comments' fixedWidth />
</button> </button>
<button <button
className={selectedFilter === 'favourite' ? 'active' : ''} className={selectedFilter === 'favourite' ? 'active' : ''}
onClick={this.onClick('favourite')} onClick={this.onClick('favourite')}
title={intl.formatMessage(tooltips.favourites)} title={intl.formatMessage(tooltips.favourites)}
> >
<Icon id='star' fixedWidth />
<Icon id='heart' fixedWidth />
</button> </button>
<button <button
className={selectedFilter === 'reblog' ? 'active' : ''} className={selectedFilter === 'reblog' ? 'active' : ''}

+ 1
- 1
app/javascript/mastodon/features/notifications/components/notification.js View File

@ -185,7 +185,7 @@ class Notification extends ImmutablePureComponent {
<div className={classNames('notification notification-favourite focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}> <div className={classNames('notification notification-favourite focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, intl.formatMessage(messages.favourite, { name: notification.getIn(['account', 'acct']) }), notification.get('created_at'))}>
<div className='notification__message'> <div className='notification__message'>
<div className='notification__favourite-icon-wrapper'> <div className='notification__favourite-icon-wrapper'>
<Icon id='star' className='star-icon' fixedWidth />
<Icon id='heart' className='star-icon' fixedWidth />
</div> </div>
<span title={notification.get('created_at')}> <span title={notification.get('created_at')}>

+ 6
- 5
app/javascript/mastodon/features/picture_in_picture/components/footer.js View File

@ -16,6 +16,7 @@ import { openModal } from 'mastodon/actions/modal';
const messages = defineMessages({ const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' }, replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@ -132,11 +133,11 @@ class Footer extends ImmutablePureComponent {
let replyIcon, replyTitle; let replyIcon, replyTitle;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else {
replyIcon = 'reply'; replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply); replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
} }
let reblogTitle = ''; let reblogTitle = '';
@ -153,9 +154,9 @@ class Footer extends ImmutablePureComponent {
return ( return (
<div className='picture-in-picture__footer'> <div className='picture-in-picture__footer'>
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} obfuscateCount />
<IconButton className='status__action-bar-button' title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'comment-o' : replyIcon} onClick={this.handleReplyClick} counter={status.get('replies_count')} />
<IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} /> <IconButton className={classNames('status__action-bar-button', { reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} pressed={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} counter={status.get('reblogs_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} counter={status.get('favourites_count')} />
<IconButton className='status__action-bar-button star-icon' animate active={status.get('favourited')} pressed={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='heart' counter={status.get('favourites_count')} />
{withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />} {withOpenButton && <IconButton className='status__action-bar-button' title={intl.formatMessage(messages.open)} icon='external-link' onClick={this.handleOpenClick} />}
</div> </div>
); );

+ 9
- 6
app/javascript/mastodon/features/status/components/action_bar.js View File

@ -14,6 +14,7 @@ const messages = defineMessages({
direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' }, direct: { id: 'status.direct', defaultMessage: 'Direct message @{name}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' }, mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' }, reply: { id: 'status.reply', defaultMessage: 'Reply' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' }, reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' }, reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' }, cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@ -252,11 +253,13 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div> <div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
); );
let replyIcon;
let replyIcon, replyTitle;
if (status.get('in_reply_to_id', null) === null) { if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'reply';
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else { } else {
replyIcon = 'reply-all';
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} }
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private'; const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
@ -274,9 +277,9 @@ class ActionBar extends React.PureComponent {
return ( return (
<div className='detailed-status__action-bar'> <div className='detailed-status__action-bar'>
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.reply)} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'reply' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button' ><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='star' onClick={this.handleFavouriteClick} /></div>
<div className='detailed-status__button'><IconButton title={replyTitle} icon={status.get('in_reply_to_account_id') === status.getIn(['account', 'id']) ? 'comment-o' : replyIcon} onClick={this.handleReplyClick} /></div>
<div className='detailed-status__button'><IconButton className={classNames({ reblogPrivate })} disabled={!publicStatus && !reblogPrivate} active={status.get('reblogged')} title={reblogTitle} icon='retweet' onClick={this.handleReblogClick} /></div>
<div className='detailed-status__button'><IconButton className='star-icon' animate active={status.get('favourited')} title={intl.formatMessage(messages.favourite)} icon='heart' onClick={this.handleFavouriteClick} /></div>
{shareButton} {shareButton}
<div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>

+ 18
- 5
app/javascript/mastodon/features/status/components/detailed_status.js View File

@ -6,7 +6,7 @@ import DisplayName from '../../../components/display_name';
import StatusContent from '../../../components/status_content'; import StatusContent from '../../../components/status_content';
import MediaGallery from '../../../components/media_gallery'; import MediaGallery from '../../../components/media_gallery';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { injectIntl, defineMessages, FormattedDate } from 'react-intl';
import { injectIntl, defineMessages, FormattedDate, FormattedNumber } from 'react-intl';
import Card from './card'; import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video'; import Video from '../../video';
@ -105,7 +105,7 @@ class DetailedStatus extends ImmutablePureComponent {
render () { render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' }; const outerStyle = { boxSizing: 'border-box' };
const { intl, compact, pictureInPicture } = this.props;
const { intl, compact, pictureInPicture, deep } = this.props;
if (!status) { if (!status) {
return null; return null;
@ -220,7 +220,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (this.context.router) { if (this.context.router) {
favouriteLink = ( favouriteLink = (
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'> <Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
<Icon id='star' />
<Icon id='heart' />
<span className='detailed-status__favorites'> <span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} /> <AnimatedNumber value={status.get('favourites_count')} />
</span> </span>
@ -229,7 +229,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else { } else {
favouriteLink = ( favouriteLink = (
<a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}> <a href={`/interact/${status.get('id')}?type=favourite`} className='detailed-status__link' onClick={this.handleModalLink}>
<Icon id='star' />
<Icon id='heart' />
<span className='detailed-status__favorites'> <span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} /> <AnimatedNumber value={status.get('favourites_count')} />
</span> </span>
@ -237,6 +237,18 @@ class DetailedStatus extends ImmutablePureComponent {
); );
} }
let deepRec;
if(deep != null) {
deepRec = (
<div className="detailed-status__button deep__number">
<Icon id="tree" />
<span>
<FormattedNumber value={deep} />
</span>
</div>
);
}
return ( return (
<div style={outerStyle}> <div style={outerStyle}>
<div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}> <div ref={this.setRef} className={classNames('detailed-status', `detailed-status-${status.get('visibility')}`, { compact })}>
@ -245,13 +257,14 @@ class DetailedStatus extends ImmutablePureComponent {
<DisplayName account={status.get('account')} localDomain={this.props.domain} /> <DisplayName account={status.get('account')} localDomain={this.props.domain} />
</a> </a>
{deepRec}
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} /> <StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
{media} {media}
<div className='detailed-status__meta'> <div className='detailed-status__meta'>
<a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'> <a className='detailed-status__datetime' href={status.get('url')} target='_blank' rel='noopener noreferrer'>
<FormattedDate value={new Date(status.get('created_at'))} hour12={false} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
<FormattedDate value={new Date(status.get('created_at'))} hour12={true} year='numeric' month='short' day='2-digit' hour='2-digit' minute='2-digit' />
</a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink} </a>{visibilityLink}{applicationLink}{reblogLink} · {favouriteLink}
</div> </div>
</div> </div>

+ 94
- 11
app/javascript/mastodon/features/status/index.js View File

@ -10,6 +10,7 @@ import MissingIndicator from '../../components/missing_indicator';
import DetailedStatus from './components/detailed_status'; import DetailedStatus from './components/detailed_status';
import ActionBar from './components/action_bar'; import ActionBar from './components/action_bar';
import Column from '../ui/components/column'; import Column from '../ui/components/column';
import Tree from 'react-tree-graph';
import { import {
favourite, favourite,
unfavourite, unfavourite,
@ -53,7 +54,7 @@ import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component'; import ImmutablePureComponent from 'react-immutable-pure-component';
import { HotKeys } from 'react-hotkeys'; import { HotKeys } from 'react-hotkeys';
import { boostModal, deleteModal } from '../../initial_state';
import { boostModal, deleteModal, treeAcct } from '../../initial_state';
import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen'; import { attachFullscreenListener, detachFullscreenListener, isFullscreen } from '../ui/util/fullscreen';
import { textForScreenReader, defaultMediaVisibility } from '../../components/status'; import { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
@ -128,22 +129,60 @@ const makeMapStateToProps = () => {
return Immutable.List(descendantsIds); return Immutable.List(descendantsIds);
}); });
const getTreeData = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
state => state.get('statuses'),
], (statusId, contextReplies, statuses) => {
const getMore = (id, notRoot) => {
const replies = contextReplies.get(id);
const cur_status = statuses.get(id);
const text = cur_status.get('search_index').replace(/@\S+?\s/,'@..');
return {
statusId: id,
name: (text.length > 16 ? text.slice(0,13) + "..." : text) + (cur_status.get('media_attachments').size > 0 ? " [图片]" : ""),
children: replies ? Array.from(replies.map( i => getMore(i, true) )) : [],
}
}
let treeData = getMore(statusId, false)
return treeData;
});
const mapStateToProps = (state, props) => { const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId }); const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List(); let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List(); let descendantsIds = Immutable.List();
let rootAcct;
let deep;
let treeData;
if (status) { if (status) {
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
const root_status = ancestorsIds.size? getStatus(state, {id: ancestorsIds.get(0)}) : status;
rootAcct = root_status? root_status.getIn(['account', 'id']) : null;
if(rootAcct == treeAcct) {
deep = ancestorsIds.size;
descendantsIds = state.getIn(['contexts', 'replies', status.get('id')]);
if(descendantsIds)
descendantsIds = descendantsIds.reverse();
}
else {
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
treeData = getTreeData(state, {id: status.get('id')})
} }
return { return {
status, status,
deep,
ancestorsIds, ancestorsIds,
descendantsIds, descendantsIds,
treeData,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0, askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']), domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
@ -181,6 +220,9 @@ class Status extends ImmutablePureComponent {
fullscreen: false, fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status),
loadedStatusId: undefined, loadedStatusId: undefined,
showTree: false,
svgWidth: 400,
activeNode: null
}; };
componentWillMount () { componentWillMount () {
@ -334,6 +376,13 @@ class Status extends ImmutablePureComponent {
} }
} }
handleShowTree = () => {
this.setState({
activeNode: null,
showTree: !this.state.showTree
});
}
handleBlockClick = (status) => { handleBlockClick = (status) => {
const { dispatch } = this.props; const { dispatch } = this.props;
const account = status.get('account'); const account = status.get('account');
@ -455,14 +504,17 @@ class Status extends ImmutablePureComponent {
} }
} }
renderChildren (list) {
return list.map(id => (
renderChildren (list, type) {
const { deep } = this.props;
return list.map((id,idx) => (
<StatusContainer <StatusContainer
key={id} key={id}
id={id} id={id}
onMoveUp={this.handleMoveUp} onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown} onMoveDown={this.handleMoveDown}
contextType='thread' contextType='thread'
deep={deep==null? null : (type == 'ance'? idx : deep+1)}
tree_type={deep==null? null : type}
/> />
)); ));
} }
@ -496,10 +548,19 @@ class Status extends ImmutablePureComponent {
this.setState({ fullscreen: isFullscreen() }); this.setState({ fullscreen: isFullscreen() });
} }
handleNodeClick = (ev, node) => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${node}`);
}
render () { render () {
let ancestors, descendants; let ancestors, descendants;
const { status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
const { status, deep, ancestorsIds, descendantsIds, treeData, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen, showTree, svgWidth, activeNode } = this.state;
if (status === null) { if (status === null) {
return ( return (
@ -511,11 +572,11 @@ class Status extends ImmutablePureComponent {
} }
if (ancestorsIds && ancestorsIds.size > 0) { if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
ancestors = <div>{this.renderChildren(ancestorsIds, 'ance')}</div>;
} }
if (descendantsIds && descendantsIds.size > 0) { if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>;
descendants = <div>{this.renderChildren(descendantsIds, 'desc')}</div>;
} }
const handlers = { const handlers = {
@ -537,19 +598,40 @@ class Status extends ImmutablePureComponent {
showBackButton showBackButton
multiColumn={multiColumn} multiColumn={multiColumn}
extraButton={( extraButton={(
<button className='column-header__button' title={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} aria-label={intl.formatMessage(status.get('hidden') ? messages.revealAll : messages.hideAll)} onClick={this.handleToggleAll} aria-pressed={status.get('hidden') ? 'false' : 'true'}><Icon id={status.get('hidden') ? 'eye-slash' : 'eye'} /></button>
<button className='column-header__button' onClick={this.handleShowTree}><Icon id={this.state.showTree ? 'eye-slash' : 'eye'} /></button>
)} )}
/> />
<ScrollContainer scrollKey='thread'> <ScrollContainer scrollKey='thread'>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
<div className={classNames('scrollable', { fullscreen }, {'tree':deep!=null})} ref={this.setRef}>
{ancestors} {ancestors}
<HotKeys handlers={handlers}> <HotKeys handlers={handlers}>
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)}>
<div className={classNames('focusable', 'detailed-status__wrapper')} tabIndex='0' aria-label={textForScreenReader(intl, status, false)} ref={e=>{if(e) this.setState({svgWidth: e.clientWidth})}}>
{showTree ?
<Tree
data={treeData}
height={800}
width={svgWidth+80}
animated
keyProp = {"statusId"}
textProps={{
x: -9,
y: -7
}}
gProps={{
className: 'node',
onClick: this.handleNodeClick
}}
svgProps={{
className: 'tree-svg'
}}/>
:
<DetailedStatus <DetailedStatus
key={`details-${status.get('id')}`} key={`details-${status.get('id')}`}
status={status} status={status}
deep={deep}
onOpenVideo={this.handleOpenVideo} onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia} onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden} onToggleHidden={this.handleToggleHidden}
@ -558,6 +640,7 @@ class Status extends ImmutablePureComponent {
onToggleMediaVisibility={this.handleToggleMediaVisibility} onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture} pictureInPicture={pictureInPicture}
/> />
}
<ActionBar <ActionBar
key={`action-bar-${status.get('id')}`} key={`action-bar-${status.get('id')}`}

+ 6
- 0
app/javascript/mastodon/features/ui/components/columns_area.js View File

@ -34,6 +34,8 @@ import NavigationPanel from './navigation_panel';
import { supportsPassiveEvents } from 'detect-passive-events'; import { supportsPassiveEvents } from 'detect-passive-events';
import { scrollRight } from '../../../scroll'; import { scrollRight } from '../../../scroll';
import TrendsContainer from '../../getting_started/containers/trends_container';
const componentMap = { const componentMap = {
'COMPOSE': Compose, 'COMPOSE': Compose,
'HOME': HomeTimeline, 'HOME': HomeTimeline,
@ -196,6 +198,10 @@ class ColumnsArea extends ImmutablePureComponent {
return ( return (
<div className='columns-area columns-area--mobile' key={index}> <div className='columns-area columns-area--mobile' key={index}>
{
['column.community', 'column.public'].includes(link.props['data-preview-title-id']) &&
<TrendsContainer />
}
{view} {view}
</div> </div>
); );

+ 1
- 1
app/javascript/mastodon/features/ui/components/link_footer.js View File

@ -52,7 +52,7 @@ class LinkFooter extends React.PureComponent {
{withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>} {withHotkeys && <li><Link to='/keyboard-shortcuts'><FormattedMessage id='navigation_bar.keyboard_shortcuts' defaultMessage='Hotkeys' /></Link> · </li>}
<li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li> <li><a href='/auth/edit'><FormattedMessage id='getting_started.security' defaultMessage='Security' /></a> · </li>
<li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li> <li><a href='/about/more' target='_blank'><FormattedMessage id='navigation_bar.info' defaultMessage='About this server' /></a> · </li>
<li><a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='https://closed.social/apps' target='_blank'><FormattedMessage id='navigation_bar.apps' defaultMessage='Mobile apps' /></a> · </li>
<li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li> <li><a href='/terms' target='_blank'><FormattedMessage id='getting_started.terms' defaultMessage='Terms of service' /></a> · </li>
<li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li> <li><a href='/settings/applications' target='_blank'><FormattedMessage id='getting_started.developers' defaultMessage='Developers' /></a> · </li>
<li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li> <li><a href='https://docs.joinmastodon.org' target='_blank'><FormattedMessage id='getting_started.documentation' defaultMessage='Documentation' /></a> · </li>

+ 5
- 2
app/javascript/mastodon/features/ui/components/navigation_panel.js View File

@ -2,7 +2,7 @@ import React from 'react';
import { NavLink, withRouter } from 'react-router-dom'; import { NavLink, withRouter } from 'react-router-dom';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import { profile_directory, showTrends } from 'mastodon/initial_state';
import { profile_directory, showTrends, treeRoot } from 'mastodon/initial_state';
import NotificationsCounterIcon from './notifications_counter_icon'; import NotificationsCounterIcon from './notifications_counter_icon';
import FollowRequestsNavLink from './follow_requests_nav_link'; import FollowRequestsNavLink from './follow_requests_nav_link';
import ListPanel from './list_panel'; import ListPanel from './list_panel';
@ -11,12 +11,15 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
const NavigationPanel = () => ( const NavigationPanel = () => (
<div className='navigation-panel'> <div className='navigation-panel'>
<NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink> <NavLink className='column-link column-link--transparent' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon className='column-link__icon' id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>
{treeRoot && (
<NavLink className='column-link column-link--transparent' to={treeRoot} data-preview-title-id='column.tree' data-preview-icon='tree' ><Icon className='column-link__icon' id='tree' fixedWidth /><FormattedMessage id='tabs_bar.tree' defaultMessage='Tree' /></NavLink>
)}
<NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink> <NavLink className='column-link column-link--transparent' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon className='column-link__icon' /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>
<FollowRequestsNavLink /> <FollowRequestsNavLink />
<NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink> <NavLink className='column-link column-link--transparent' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon className='column-link__icon' id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>
<NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink> <NavLink className='column-link column-link--transparent' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon className='column-link__icon' id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink> <NavLink className='column-link column-link--transparent' to='/timelines/direct'><Icon className='column-link__icon' id='envelope' fixedWidth /><FormattedMessage id='navigation_bar.direct' defaultMessage='Direct messages' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='star' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/favourites'><Icon className='column-link__icon' id='heart' fixedWidth /><FormattedMessage id='navigation_bar.favourites' defaultMessage='Favourites' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink> <NavLink className='column-link column-link--transparent' to='/bookmarks'><Icon className='column-link__icon' id='bookmark' fixedWidth /><FormattedMessage id='navigation_bar.bookmarks' defaultMessage='Bookmarks' /></NavLink>
<NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink> <NavLink className='column-link column-link--transparent' to='/lists'><Icon className='column-link__icon' id='list-ul' fixedWidth /><FormattedMessage id='navigation_bar.lists' defaultMessage='Lists' /></NavLink>
{profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>} {profile_directory && <NavLink className='column-link column-link--transparent' to='/directory'><Icon className='column-link__icon' id='address-book-o' fixedWidth /><FormattedMessage id='getting_started.directory' defaultMessage='Profile directory' /></NavLink>}

+ 15
- 1
app/javascript/mastodon/features/ui/components/tabs_bar.js View File

@ -7,14 +7,19 @@ import { isUserTouching } from '../../../is_mobile';
import Icon from 'mastodon/components/icon'; import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon'; import NotificationsCounterIcon from './notifications_counter_icon';
import { treeRoot } from '../../../initial_state';
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
export const links = [ export const links = [
<NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>, <NavLink className='tabs-bar__link' to='/timelines/home' data-preview-title-id='column.home' data-preview-icon='home' ><Icon id='home' fixedWidth /><FormattedMessage id='tabs_bar.home' defaultMessage='Home' /></NavLink>,
treeRoot && (
<NavLink className='tabs-bar__link' to={treeRoot} data-preview-title-id='column.tree' data-preview-icon='tree' ><Icon id='tree' fixedWidth /><FormattedMessage id='tabs_bar.tree' defaultMessage='Tree' /></NavLink>),
<NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>, <NavLink className='tabs-bar__link' to='/notifications' data-preview-title-id='column.notifications' data-preview-icon='bell' ><NotificationsCounterIcon /><FormattedMessage id='tabs_bar.notifications' defaultMessage='Notifications' /></NavLink>,
<NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>, <NavLink className='tabs-bar__link' to='/timelines/public/local' data-preview-title-id='column.community' data-preview-icon='users' ><Icon id='users' fixedWidth /><FormattedMessage id='tabs_bar.local_timeline' defaultMessage='Local' /></NavLink>,
<NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>, <NavLink className='tabs-bar__link' exact to='/timelines/public' data-preview-title-id='column.public' data-preview-icon='globe' ><Icon id='globe' fixedWidth /><FormattedMessage id='tabs_bar.federated_timeline' defaultMessage='Federated' /></NavLink>,
<NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>, <NavLink className='tabs-bar__link optional' to='/search' data-preview-title-id='tabs_bar.search' data-preview-icon='bell' ><Icon id='search' fixedWidth /><FormattedMessage id='tabs_bar.search' defaultMessage='Search' /></NavLink>,
<NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>, <NavLink className='tabs-bar__link' style={{ flexGrow: '0', flexBasis: '30px' }} to='/getting-started' data-preview-title-id='getting_started.heading' data-preview-icon='bars' ><Icon id='bars' fixedWidth /></NavLink>,
];
].filter(q => !!q);
export function getIndex (path) { export function getIndex (path) {
return links.findIndex(link => link.props.to === path); return links.findIndex(link => link.props.to === path);
@ -33,6 +38,10 @@ class TabsBar extends React.PureComponent {
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired,
} }
state = {
showPinned: true,
}
setRef = ref => { setRef = ref => {
this.node = ref; this.node = ref;
} }
@ -69,8 +78,13 @@ class TabsBar extends React.PureComponent {
} }
handleClear = () => {
this.setState({ showPinned: false});
}
render () { render () {
const { intl: { formatMessage } } = this.props; const { intl: { formatMessage } } = this.props;
const { showPinned } = this.state;
return ( return (
<div className='tabs-bar__wrapper'> <div className='tabs-bar__wrapper'>

+ 3
- 0
app/javascript/mastodon/initial_state.js View File

@ -27,4 +27,7 @@ export const title = getMeta('title');
export const cropImages = getMeta('crop_images'); export const cropImages = getMeta('crop_images');
export const disableSwiping = getMeta('disable_swiping'); export const disableSwiping = getMeta('disable_swiping');
export const treeRoot = getMeta('tree_root');
export const treeAcct = getMeta('tree_acct')
export default initialState; export default initialState;

+ 5
- 2
app/javascript/mastodon/locales/en.json View File

@ -69,6 +69,7 @@
"column.favourites": "Favourites", "column.favourites": "Favourites",
"column.follow_requests": "Follow requests", "column.follow_requests": "Follow requests",
"column.home": "Home", "column.home": "Home",
"column.tree": "Tree",
"column.lists": "Lists", "column.lists": "Lists",
"column.mutes": "Muted users", "column.mutes": "Muted users",
"column.notifications": "Notifications", "column.notifications": "Notifications",
@ -185,10 +186,10 @@
"generic.saved": "Saved", "generic.saved": "Saved",
"getting_started.developers": "Developers", "getting_started.developers": "Developers",
"getting_started.directory": "Profile directory", "getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
"getting_started.documentation": "Mastodon Documentation",
"getting_started.heading": "Getting started", "getting_started.heading": "Getting started",
"getting_started.invite": "Invite people", "getting_started.invite": "Invite people",
"getting_started.open_source_notice": "Mastodon is open source software. You can contribute or report issues on GitHub at {github}.",
"getting_started.open_source_notice": "ClosedSocial is open source software. You can contribute or report issues on GitHub at {github}.",
"getting_started.security": "Account settings", "getting_started.security": "Account settings",
"getting_started.terms": "Terms of service", "getting_started.terms": "Terms of service",
"hashtag.column_header.tag_mode.all": "and {additional}", "hashtag.column_header.tag_mode.all": "and {additional}",
@ -384,6 +385,7 @@
"status.bookmark": "Bookmark", "status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost", "status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted", "status.cannot_reblog": "This post cannot be boosted",
"status.comment": "Comment",
"status.copy": "Copy link to post", "status.copy": "Copy link to post",
"status.delete": "Delete", "status.delete": "Delete",
"status.detailed_status": "Detailed conversation view", "status.detailed_status": "Detailed conversation view",
@ -424,6 +426,7 @@
"suggestions.header": "You might be interested in…", "suggestions.header": "You might be interested in…",
"tabs_bar.federated_timeline": "Federated", "tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home", "tabs_bar.home": "Home",
"tabs_bar.tree": "Tree",
"tabs_bar.local_timeline": "Local", "tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications", "tabs_bar.notifications": "Notifications",
"tabs_bar.search": "Search", "tabs_bar.search": "Search",

+ 5
- 2
app/javascript/mastodon/locales/zh-CN.json View File

@ -69,6 +69,7 @@
"column.favourites": "喜欢", "column.favourites": "喜欢",
"column.follow_requests": "关注请求", "column.follow_requests": "关注请求",
"column.home": "主页", "column.home": "主页",
"column.tree": "闭社树",
"column.lists": "列表", "column.lists": "列表",
"column.mutes": "已隐藏的用户", "column.mutes": "已隐藏的用户",
"column.notifications": "通知", "column.notifications": "通知",
@ -185,10 +186,10 @@
"generic.saved": "已保存", "generic.saved": "已保存",
"getting_started.developers": "开发", "getting_started.developers": "开发",
"getting_started.directory": "用户目录", "getting_started.directory": "用户目录",
"getting_started.documentation": "文档",
"getting_started.documentation": "Mastodon文档",
"getting_started.heading": "开始使用", "getting_started.heading": "开始使用",
"getting_started.invite": "邀请用户", "getting_started.invite": "邀请用户",
"getting_started.open_source_notice": "Mastodon 是开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。",
"getting_started.open_source_notice": "闭社是开源软件。欢迎前往 GitHub({github})贡献代码或反馈问题。",
"getting_started.security": "帐户安全", "getting_started.security": "帐户安全",
"getting_started.terms": "使用条款", "getting_started.terms": "使用条款",
"hashtag.column_header.tag_mode.all": "以及 {additional}", "hashtag.column_header.tag_mode.all": "以及 {additional}",
@ -406,6 +407,7 @@
"status.reblogged_by": "{name} 转嘟了", "status.reblogged_by": "{name} 转嘟了",
"status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。", "status.reblogs.empty": "没有人转嘟过此条嘟文。如果有人转嘟了,就会显示在这里。",
"status.redraft": "删除并重新编辑", "status.redraft": "删除并重新编辑",
"status.comment": "评论",
"status.remove_bookmark": "移除书签", "status.remove_bookmark": "移除书签",
"status.reply": "回复", "status.reply": "回复",
"status.replyAll": "回复所有人", "status.replyAll": "回复所有人",
@ -424,6 +426,7 @@
"suggestions.header": "你可能会感兴趣…", "suggestions.header": "你可能会感兴趣…",
"tabs_bar.federated_timeline": "跨站", "tabs_bar.federated_timeline": "跨站",
"tabs_bar.home": "主页", "tabs_bar.home": "主页",
"tabs_bar.tree": "闭社树",
"tabs_bar.local_timeline": "本站", "tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知", "tabs_bar.notifications": "通知",
"tabs_bar.search": "搜索", "tabs_bar.search": "搜索",

+ 11
- 8
app/javascript/mastodon/locales/zh-HK.json View File

@ -74,6 +74,7 @@
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "置頂文章", "column.pins": "置頂文章",
"column.public": "跨站時間軸", "column.public": "跨站時間軸",
"column.tree": "閉社樹",
"column_back_button.label": "返回", "column_back_button.label": "返回",
"column_header.hide_settings": "隱藏設定", "column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移", "column_header.moveLeft_settings": "將欄左移",
@ -158,8 +159,8 @@
"empty_column.community": "本站時間軸暫時未有內容,快寫一點東西來搶頭香啊!", "empty_column.community": "本站時間軸暫時未有內容,快寫一點東西來搶頭香啊!",
"empty_column.direct": "你沒有個人訊息。當你發出或接收個人訊息,就會在這裡出現。", "empty_column.direct": "你沒有個人訊息。當你發出或接收個人訊息,就會在這裡出現。",
"empty_column.domain_blocks": "尚未隱藏任何網域。", "empty_column.domain_blocks": "尚未隱藏任何網域。",
"empty_column.favourited_statuses": "你還沒收藏任何文章。這裡將會顯示你收藏的嘟文。",
"empty_column.favourites": "還沒有人收藏這則文章。這裡將會顯示被收藏的嘟文。",
"empty_column.favourited_statuses": "你還沒喜歡任何嘟文。這裡將會顯示你喜歡的嘟文。",
"empty_column.favourites": "還沒有人喜歡這則嘟文。這裡將會顯示被喜歡的嘟文。",
"empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.", "empty_column.follow_recommendations": "Looks like no suggestions could be generated for you. You can try using search to look for people you might know or explore trending hashtags.",
"empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。", "empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
"empty_column.hashtag": "這個標籤暫時未有內容。", "empty_column.hashtag": "這個標籤暫時未有內容。",
@ -185,11 +186,11 @@
"generic.saved": "已儲存", "generic.saved": "已儲存",
"getting_started.developers": "開發者", "getting_started.developers": "開發者",
"getting_started.directory": "個人資料目錄", "getting_started.directory": "個人資料目錄",
"getting_started.documentation": "文件",
"getting_started.documentation": "Mastodon文件",
"getting_started.heading": "開始使用", "getting_started.heading": "開始使用",
"getting_started.invite": "邀請使用者", "getting_started.invite": "邀請使用者",
"getting_started.open_source_notice": "Mastodon(萬象)是一個開放源碼的軟件。你可以在官方 GitHub {github} 貢獻或者回報問題。",
"getting_started.security": "帳戶設定",
"getting_started.open_source_notice": "閉社是一個開放源碼的軟件。你可以在官方 GitHub ({github}) 貢獻或者回報問題。",
"getting_started.security": "帳戶安全",
"getting_started.terms": "服務條款", "getting_started.terms": "服務條款",
"hashtag.column_header.tag_mode.all": "以及{additional}", "hashtag.column_header.tag_mode.all": "以及{additional}",
"hashtag.column_header.tag_mode.any": "或是{additional}", "hashtag.column_header.tag_mode.any": "或是{additional}",
@ -217,7 +218,7 @@
"keyboard_shortcuts.direct": "開啟私訊欄", "keyboard_shortcuts.direct": "開啟私訊欄",
"keyboard_shortcuts.down": "在列表往下移動", "keyboard_shortcuts.down": "在列表往下移動",
"keyboard_shortcuts.enter": "打開文章", "keyboard_shortcuts.enter": "打開文章",
"keyboard_shortcuts.favourite": "收藏文章",
"keyboard_shortcuts.favourite": "喜歡",
"keyboard_shortcuts.favourites": "開啟最愛的內容", "keyboard_shortcuts.favourites": "開啟最愛的內容",
"keyboard_shortcuts.federated": "打開跨站時間軸", "keyboard_shortcuts.federated": "打開跨站時間軸",
"keyboard_shortcuts.heading": "鍵盤快速鍵", "keyboard_shortcuts.heading": "鍵盤快速鍵",
@ -368,7 +369,7 @@
"report.target": "舉報", "report.target": "舉報",
"search.placeholder": "搜尋", "search.placeholder": "搜尋",
"search_popout.search_format": "高級搜索格式", "search_popout.search_format": "高級搜索格式",
"search_popout.tips.full_text": "輸入簡單的文字,搜索由你發放、收藏、轉推和提及你的文章,以及符合的使用者名稱,顯示名稱和標籤。",
"search_popout.tips.full_text": "輸入簡單的文字,搜索由你發放、喜歡、轉推和提及你的文章,以及符合的用戶名稱,帳號名稱和標籤。",
"search_popout.tips.hashtag": "標籤", "search_popout.tips.hashtag": "標籤",
"search_popout.tips.status": "文章", "search_popout.tips.status": "文章",
"search_popout.tips.text": "輸入簡單的文字,搜索符合的顯示名稱、使用者名稱和標籤", "search_popout.tips.text": "輸入簡單的文字,搜索符合的顯示名稱、使用者名稱和標籤",
@ -384,7 +385,8 @@
"status.bookmark": "書籤", "status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉推", "status.cancel_reblog_private": "取消轉推",
"status.cannot_reblog": "這篇文章無法被轉推", "status.cannot_reblog": "這篇文章無法被轉推",
"status.copy": "將連結複製到文章中",
"status.comment": "評論",
"status.copy": "將連結複製到嘟文中",
"status.delete": "刪除", "status.delete": "刪除",
"status.detailed_status": "詳細對話內容", "status.detailed_status": "詳細對話內容",
"status.direct": "私訊 @{name}", "status.direct": "私訊 @{name}",
@ -427,6 +429,7 @@
"tabs_bar.local_timeline": "本站", "tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知", "tabs_bar.notifications": "通知",
"tabs_bar.search": "搜尋", "tabs_bar.search": "搜尋",
"tabs_bar.tree": "閉社樹",
"time_remaining.days": "剩餘 {number, plural, one {# 天} other {# 天}}", "time_remaining.days": "剩餘 {number, plural, one {# 天} other {# 天}}",
"time_remaining.hours": "剩餘 {number, plural, one {# 小時} other {# 小時}}", "time_remaining.hours": "剩餘 {number, plural, one {# 小時} other {# 小時}}",
"time_remaining.minutes": "剩餘 {number, plural, one {# 分鐘} other {# 分鐘}}", "time_remaining.minutes": "剩餘 {number, plural, one {# 分鐘} other {# 分鐘}}",

+ 15
- 11
app/javascript/mastodon/locales/zh-TW.json View File

@ -65,8 +65,9 @@
"column.community": "本機時間軸", "column.community": "本機時間軸",
"column.direct": "私訊", "column.direct": "私訊",
"column.directory": "瀏覽個人資料", "column.directory": "瀏覽個人資料",
"column.domain_blocks": "隱藏的網域",
"column.domain_blocks": "已封鎖的網域", "column.domain_blocks": "已封鎖的網域",
"column.favourites": "收藏",
"column.favourites": "喜歡",
"column.follow_requests": "關注請求", "column.follow_requests": "關注請求",
"column.home": "首頁", "column.home": "首頁",
"column.lists": "名單", "column.lists": "名單",
@ -74,6 +75,7 @@
"column.notifications": "通知", "column.notifications": "通知",
"column.pins": "釘選的嘟文", "column.pins": "釘選的嘟文",
"column.public": "聯邦時間軸", "column.public": "聯邦時間軸",
"column.tree" : "閉社樹",
"column_back_button.label": "上一頁", "column_back_button.label": "上一頁",
"column_header.hide_settings": "隱藏設定", "column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄位向左移動", "column_header.moveLeft_settings": "將欄位向左移動",
@ -121,7 +123,7 @@
"confirmations.mute.explanation": "這將會隱藏來自他們的貼文與通知,但是他們還是可以查閱你的貼文與關注您。", "confirmations.mute.explanation": "這將會隱藏來自他們的貼文與通知,但是他們還是可以查閱你的貼文與關注您。",
"confirmations.mute.message": "確定靜音 {name} ?", "confirmations.mute.message": "確定靜音 {name} ?",
"confirmations.redraft.confirm": "刪除並重新編輯", "confirmations.redraft.confirm": "刪除並重新編輯",
"confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及收藏,且回覆這則的嘟文將會變成獨立的嘟文。",
"confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及喜歡,且回覆這則的嘟文將會變成獨立的嘟文。",
"confirmations.reply.confirm": "回覆", "confirmations.reply.confirm": "回覆",
"confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?", "confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?",
"confirmations.unfollow.confirm": "取消關注", "confirmations.unfollow.confirm": "取消關注",
@ -158,8 +160,8 @@
"empty_column.community": "本機時間軸是空的。快公開嘟些文搶頭香啊!", "empty_column.community": "本機時間軸是空的。快公開嘟些文搶頭香啊!",
"empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。", "empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。",
"empty_column.domain_blocks": "尚未封鎖任何網域。", "empty_column.domain_blocks": "尚未封鎖任何網域。",
"empty_column.favourited_statuses": "您還沒收藏過任何嘟文。當您收藏嘟文時,它將於此顯示。",
"empty_column.favourites": "還沒有人收藏過這則嘟文。當有人收藏嘟文時,它將於此顯示。",
"empty_column.favourited_statuses": "你還沒喜歡任何嘟文。這裡將會顯示你喜歡的嘟文。",
"empty_column.favourites": "還沒有人喜歡這則嘟文。這裡將會顯示被喜歡的嘟文。",
"empty_column.follow_recommendations": "似乎未能為您生成任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。", "empty_column.follow_recommendations": "似乎未能為您生成任何建議。您可以嘗試使用搜尋來尋找您可能認識的人,或是探索熱門主題標籤。",
"empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。", "empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
"empty_column.hashtag": "這個主題標籤下什麼也沒有。", "empty_column.hashtag": "這個主題標籤下什麼也沒有。",
@ -185,11 +187,11 @@
"generic.saved": "已儲存", "generic.saved": "已儲存",
"getting_started.developers": "開發者", "getting_started.developers": "開發者",
"getting_started.directory": "個人資料目錄", "getting_started.directory": "個人資料目錄",
"getting_started.documentation": "文件",
"getting_started.documentation": "Mastodon文件",
"getting_started.heading": "開始使用", "getting_started.heading": "開始使用",
"getting_started.invite": "邀請使用者", "getting_started.invite": "邀請使用者",
"getting_started.open_source_notice": "Mastodon 是開源軟體。您可以在 GitHub {github} 上貢獻或是回報問題。",
"getting_started.security": "帳安全性設定",
"getting_started.open_source_notice": "閉社是開源軟體。你可以在 GitHub {github} 上貢獻或是回報問題。",
"getting_started.security": "帳安全性設定",
"getting_started.terms": "服務條款", "getting_started.terms": "服務條款",
"hashtag.column_header.tag_mode.all": "以及 {additional}", "hashtag.column_header.tag_mode.all": "以及 {additional}",
"hashtag.column_header.tag_mode.any": "或是 {additional}", "hashtag.column_header.tag_mode.any": "或是 {additional}",
@ -217,8 +219,8 @@
"keyboard_shortcuts.direct": "開啟私訊欄", "keyboard_shortcuts.direct": "開啟私訊欄",
"keyboard_shortcuts.down": "在名單中往下移動", "keyboard_shortcuts.down": "在名單中往下移動",
"keyboard_shortcuts.enter": "檢視嘟文", "keyboard_shortcuts.enter": "檢視嘟文",
"keyboard_shortcuts.favourite": "加到收藏",
"keyboard_shortcuts.favourites": "開啟收藏名單",
"keyboard_shortcuts.favourite": "喜歡",
"keyboard_shortcuts.favourites": "開啟喜歡名單",
"keyboard_shortcuts.federated": "開啟聯邦時間軸", "keyboard_shortcuts.federated": "開啟聯邦時間軸",
"keyboard_shortcuts.heading": "鍵盤快速鍵", "keyboard_shortcuts.heading": "鍵盤快速鍵",
"keyboard_shortcuts.home": "開啟首頁時間軸", "keyboard_shortcuts.home": "開啟首頁時間軸",
@ -277,7 +279,7 @@
"navigation_bar.discover": "探索", "navigation_bar.discover": "探索",
"navigation_bar.domain_blocks": "隱藏的網域", "navigation_bar.domain_blocks": "隱藏的網域",
"navigation_bar.edit_profile": "編輯個人資料", "navigation_bar.edit_profile": "編輯個人資料",
"navigation_bar.favourites": "收藏",
"navigation_bar.favourites": "喜歡",
"navigation_bar.filters": "靜音詞彙", "navigation_bar.filters": "靜音詞彙",
"navigation_bar.follow_requests": "關注請求", "navigation_bar.follow_requests": "關注請求",
"navigation_bar.follows_and_followers": "關注及關注者", "navigation_bar.follows_and_followers": "關注及關注者",
@ -383,7 +385,8 @@
"status.block": "封鎖 @{name}", "status.block": "封鎖 @{name}",
"status.bookmark": "書籤", "status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉嘟", "status.cancel_reblog_private": "取消轉嘟",
"status.cannot_reblog": "這則嘟文無法被轉嘟",
"status.cannot_reblog": "這篇嘟文無法被轉嘟",
"status.comment": "評論",
"status.copy": "複製嘟文連結", "status.copy": "複製嘟文連結",
"status.delete": "刪除", "status.delete": "刪除",
"status.detailed_status": "詳細的對話內容", "status.detailed_status": "詳細的對話內容",
@ -427,6 +430,7 @@
"tabs_bar.local_timeline": "本機", "tabs_bar.local_timeline": "本機",
"tabs_bar.notifications": "通知", "tabs_bar.notifications": "通知",
"tabs_bar.search": "搜尋", "tabs_bar.search": "搜尋",
"tabs_bar.tree": "閉社樹",
"time_remaining.days": "剩餘{number, plural, one {# 天} other {# 天}}", "time_remaining.days": "剩餘{number, plural, one {# 天} other {# 天}}",
"time_remaining.hours": "剩餘{number, plural, one {# 小時} other {# 小時}}", "time_remaining.hours": "剩餘{number, plural, one {# 小時} other {# 小時}}",
"time_remaining.minutes": "剩餘{number, plural, one {# 分鐘} other {# 分鐘}}", "time_remaining.minutes": "剩餘{number, plural, one {# 分鐘} other {# 分鐘}}",

+ 4
- 0
app/javascript/styles/application.scss View File

@ -26,3 +26,7 @@
@import 'mastodon/dashboard'; @import 'mastodon/dashboard';
@import 'mastodon/rtl'; @import 'mastodon/rtl';
@import 'mastodon/accessibility'; @import 'mastodon/accessibility';
@import 'closed-social/tree';
@import 'closed-social/timeline_comments';
@import 'closed-social/global';

+ 115
- 0
app/javascript/styles/closed-social/global.scss View File

@ -0,0 +1,115 @@
.column {
flex: 1 0 auto;
}
.pinned-info {
position: relative;
opacity: 0.85;
font-size: 15px;
padding: 10px 20px;
a {
color: #3fadfd;
}
}
.pinned-info__icon {
.fa {
position: absolute;
bottom: 10px;
right: 10px;
cursor: pointer;
}
}
div {
&.status__info {
& > a {
&.status__display-name {
display: inline-block;
}
}
}
}
.gifv {
& > video {
width: 100%;
max-height: 100%;
}
}
.columns-area--mobile {
.getting-started__trends {
display: block;
.trends__item {
display: flex;
}
}
}
.status__quote__wrapper {
margin-top:16px;
border-left: 5px solid #dbdbdb80;
background: #dbdbdb40;
.status {
padding-left:40px;
.status__action-bar {
display: none;
}
.status__avatar {
transform: scale(0.5);
transform-origin: 0% 0%;
}
}
}
.status__tree__quote__wrapper {
padding: 10px 5px;
background: #dbdbdb40;
cursor: pointer;
}
@keyframes like {
0% {
transform: scale(1);
}
25% {
transform: scale(1.75);
}
100% {
transform: scale(1);
}
}
@keyframes unlike {
0% {
transform: rotateY(0deg);
}
50% {
transform: rotateY(240deg);
}
80% {
transform: rotateY(140deg);
}
100% {
transform: rotateY(180deg);
}
}
.no-reduce-motion .icon-button.star-icon {
&.activate {
& > .fa-heart {
animation: like 1s linear;
}
}
}
.no-reduce-motion .icon-button.star-icon {
&.deactivate {
& > .fa-heart {
animation: unlike 1s linear;
}
}
}

+ 39
- 0
app/javascript/styles/closed-social/timeline_comments.scss View File

@ -0,0 +1,39 @@
.comments-timeline {
max-height: 160px;
min-width: 60%;
max-width: 100%;
overflow: hidden;
-webkit-mask-image: linear-gradient(#1a1a1a,transparent);
mask-image: linear-gradient(#1a1a1a,transparent);
transform: scale(0.85);
transform-origin: 100% 0%;
margin-bottom: -32px;
margin-right:8px;
position: relative;
z-index:9;
&:hover {
max-height: 60vh;
overflow-y: auto;
-webkit-mask-image: none;
mask-image: none;
z-index:99;
background: $tc-background;
box-shadow: $primary-text-color 3.2px 3.2px 8px;
}
&:active {
max-height: 60vh;
overflow-y: auto;
-webkit-mask-image: none;
mask-image: none;
z-index:99;
background: $tc-background;
box-shadow: $primary-text-color 3.2px 3.2px 8px;
}
& .comments-timeline-2 {
margin-left:42px;
}
}
.comments-timeline__wrapper {
height: 135px;
}

+ 65
- 0
app/javascript/styles/closed-social/tree.scss View File

@ -0,0 +1,65 @@
div.tree-ance {
.account__avatar {
display: none;
}
.display-name {
display: none;
}
}
.tree {
a.status__display-name {
>span {
>bdi {
>strong {
animation: none;
}
}
}
}
a {
>div {
>div.account__avatar {
animation: none;
}
}
}
}
.tree-ance {
background: linear-gradient(-15deg, #282C3710,90%, #197171, 95%, #0DA454);
>div.status {
padding-left: 18px;
>div.deep__number {
text-align: left;
}
}
}
.tree-desc {
background: linear-gradient(15deg, #282C3710,90%, #197171, 95%, #0da454);
>div.status {
>div.deep__number {
text-align: right;
}
}
}
svg.tree-svg {
.node {
circle {
fill: #F3F3FF;
stroke: #2593B8;
stroke-width: 1.5px;
}
text {
font-size: 11px;
background-color: #444;
fill: #F4F4F4;
text-shadow: 0 1px 4px black;
}
cursor: pointer;
}
path.link {
fill: none;
stroke: #2593B8;
stroke-width: 1.5px;
}
}

+ 1
- 0
app/javascript/styles/mastodon-light/variables.scss View File

@ -28,6 +28,7 @@ $inverted-text-color: $black !default;
$lighter-text-color: $classic-base-color !default; $lighter-text-color: $classic-base-color !default;
$light-text-color: #444b5d; $light-text-color: #444b5d;
$tc-background: $white !default;
//Newly added colors //Newly added colors
$account-background-color: $white !default; $account-background-color: $white !default;

+ 2
- 1
app/javascript/styles/mastodon/about.scss View File

@ -690,7 +690,7 @@ $small-breakpoint: 960px;
svg { svg {
fill: $primary-text-color; fill: $primary-text-color;
height: 52px;
height: 100px;
} }
@media screen and (max-width: $no-gap-breakpoint) { @media screen and (max-width: $no-gap-breakpoint) {
@ -874,6 +874,7 @@ $small-breakpoint: 960px;
color: $ui-primary-color; color: $ui-primary-color;
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: 14px;
text-align: center;
@media screen and (max-width: $no-gap-breakpoint) { @media screen and (max-width: $no-gap-breakpoint) {
position: static; position: static;

+ 1
- 1
app/javascript/styles/mastodon/forms.scss View File

@ -530,7 +530,7 @@ code {
font-family: inherit; font-family: inherit;
pointer-events: none; pointer-events: none;
cursor: default; cursor: default;
max-width: 140px;
//max-width: 140px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;

+ 4
- 1
app/javascript/styles/mastodon/variables.scss View File

@ -4,7 +4,7 @@ $white: #ffffff; // White
$success-green: #79bd9a !default; // Padua $success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise $error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange $warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$gold-star: #e04040 !default; // ::Change star to heart
$red-bookmark: $warning-red; $red-bookmark: $warning-red;
@ -43,6 +43,9 @@ $inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default; $lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default; $light-text-color: $ui-primary-color !default;
//timeline comeents
$tc-background: $classic-base-color !default;
// Language codes that uses CJK fonts // Language codes that uses CJK fonts
$cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW; $cjk-langs: ja, ko, zh-CN, zh-HK, zh-TW;

+ 3
- 0
app/javascript/styles/thu.scss View File

@ -0,0 +1,3 @@
@import 'thu/variables';
@import 'application';
@import 'thu/diff';

+ 221
- 0
app/javascript/styles/thu/diff.scss View File

@ -0,0 +1,221 @@
/* Fil */
/* Status */
/* Drawer */
body {
background: rgba(73, 58, 99, 1) url(https://www.tsinghua.edu.cn/images/nav-bg.jpg) no-repeat fixed;
background-size: cover;
background-attachment: fixed;
background-position: center;
height: 100vh !important;
}
body.theme-thu {
background: rgba(73, 58, 99, 1) url(https://www.tsinghua.edu.cn/image/nav-bg.jpg) no-repeat fixed;
background-size: cover;
background-attachment: fixed;
background-position: center;
height: 100vh !important;
}
.ui {
background: rgba(0, 0, 0, .4);
}
.column {
>.scrollable {
background: rgba(128, 112, 132, 0);
border-radius: 0 0 0.25rem 0.25rem;
color: rgba(240, 240, 240, 1);
}
}
.column-back-button {
background: rgba(240, 240, 240, 1);
box-shadow: inset 0 5px 5px rgba(0, 0, 0, 0.05);
border-bottom: 1px solid transparent;
height: auto;
}
.column-header {
background: rgba(73, 58, 99, 0.4);
border-bottom: 1px solid #aaa;
border-radius: 0.25rem 0.25rem 0 0;
}
.column-icon {
background: transparent !important;
color: rgba(255, 255, 255, .5);
}
.collapsable-collapsed {
background: transparent !important;
color: rgba(255, 255, 255, .5);
}
.column-header__button {
background: transparent !important;
color: rgba(255, 255, 255, .5);
}
.column-header__back-button {
background: transparent !important;
color: rgba(255, 255, 255, .5);
}
.column-link {
background: rgba(40, 40, 40, 0);
color: rgba(200, 200, 200, 1);
box-shadow: inset 0 5px 5px rgba(0, 0, 0, 0.05);
&:hover {
background: rgba(102, 8, 116, 0.5);
color: rgba(255, 255, 255, 1);
}
}
.drawer__header {
a {
&:hover {
background: rgba(66, 40, 72, 1);
}
}
background: transparent;
}
.getting-started {
p {
color: rgba(102, 102, 102, 1);
}
background: rgb(52, 40, 62, 0.4);
}
.static-content {
p {
margin-bottom: 0.5rem;
}
}
.column-subheading {
background: rgba(255, 255, 255, 0.2);
color: white;
}
.column-header__collapsible {
>div {
background: rgba(40, 60, 85, 0.6);
border-bottom: 1px solid;
}
}
.account__moved-note__message {
color: rgba(255, 255, 255, 1);
}
.account__moved-note {
.detailed-status__display-name {
span {
color: rgba(255, 255, 255, 1);
}
strong {
color: rgba(255, 255, 255, 0.5) !important;
}
}
}
.reply-indicator__content {
color: #DDD;
a {
color: rgba(37, 136, 208, 1);
}
.status__content__spoiler-link {
background: rgba(49, 53, 67, 1);
line-height: 1.2rem;
}
}
.status__content {
color: #DDD;
a {
color: rgba(37, 136, 208, 1);
}
.status__content__spoiler-link {
background: rgba(49, 53, 67, 1);
line-height: 1.2rem;
}
}
.status__wrapper {
border-top: 1px solid #ccc;
}
.focusable {
&:focus {
background: rgba(200, 222, 243, 0.5);
}
}
.account__header {
.icon-button {
color: rgba(255, 255, 255, 1);
color: rgba(255, 255, 255, 0.8);
&:hover {
color: rgba(255, 255, 255, 1);
}
}
background: rgba(57, 48, 59, 0);
>div {
background: rgba(57, 48, 59, 0);
}
}
.icon-button {
color: rgba(255, 255, 255, 0.6);
}
.status__display-name {
strong {
color: rgba(240, 240, 240, 1);
}
}
.account__display-name {
strong {
color: rgba(240, 240, 240, 1);
}
}
.status__content__spoiler-link {
span {
color: rgba(255, 255, 255, 1);
}
}
.account__action-bar {
.icon-button {
color: rgba(255, 255, 255, 0.8);
&:hover {
color: rgba(255, 255, 255, 1);
}
}
background: rgba(255, 255, 255, 1);
border-top: 1px solid rgba(255, 255, 255, 0.25);
border-bottom: 1px solid rgba(255, 255, 255, 0.25);
}
.account__action-bar__tab {
border-left: 1px solid rgba(255, 255, 255, 0.25);
}
.notification__message {
a {
&:hover {
color: rgba(37, 136, 208, 1);
}
}
}
.detailed-status {
background: rgba(200, 222, 243, .2);
color: rgba(51, 51, 51, 1);
}
.detailed-status__display-name {
color: rgba(255, 255, 255, 0.5);
strong {
color: rgba(255, 255, 255, 0.5);
}
}
.detailed-status__meta {
color: rgba(255, 255, 255, 0.5);
}
.detailed-status__action-bar {
background: rgba(0, 0, 0, 0.05);
border-top: 1px solid #ccc;
border-bottom: 1px solid #ccc;
box-shadow: inset 0 5px 5px rgba(0, 0, 0, 0.05);
}
.drawer__inner {
background: rgb(52, 40, 62, 0.7);
border-radius: 0.25rem;
height: auto;
max-height: 100%;
overflow-y: auto;
}
.getting-started__wrapper {
background: rgb(52, 40, 62, 0.4);
}
.pinned-info {
background: rgba(73, 58, 99, 0.7);
}
.tabs-bar__wrapper {
background: rgba(23,25,31);
}

+ 8
- 0
app/javascript/styles/thu/variables.scss View File

@ -0,0 +1,8 @@
// Dependent colors
$classic-base-color: rgba(40,44,55,0.8);
// Differences
$ui-base-color: $classic-base-color !default;
$tc-background: rgba(50,41,64,0.9) !default;

+ 1
- 1
app/lib/activitypub/activity/add.rb View File

@ -7,7 +7,7 @@ class ActivityPub::Activity::Add < ActivityPub::Activity
status = status_from_uri(object_uri) status = status_from_uri(object_uri)
status ||= fetch_remote_original_status status ||= fetch_remote_original_status
return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status)
return unless !status.nil? && status.account_id == @account.id && !@account.pinned?(status) && status.distributable?
StatusPin.create!(account: @account, status: status) StatusPin.create!(account: @account, status: status)
end end

+ 2
- 50
app/lib/activitypub/adapter.rb View File

@ -1,30 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
NAMED_CONTEXT_MAP = {
activitystreams: 'https://www.w3.org/ns/activitystreams',
security: 'https://w3id.org/security/v1',
}.freeze
CONTEXT_EXTENSION_MAP = {
manually_approves_followers: { 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers' },
sensitive: { 'sensitive' => 'as:sensitive' },
hashtag: { 'Hashtag' => 'as:Hashtag' },
moved_to: { 'movedTo' => { '@id' => 'as:movedTo', '@type' => '@id' } },
also_known_as: { 'alsoKnownAs' => { '@id' => 'as:alsoKnownAs', '@type' => '@id' } },
emoji: { 'toot' => 'http://joinmastodon.org/ns#', 'Emoji' => 'toot:Emoji' },
featured: { 'toot' => 'http://joinmastodon.org/ns#', 'featured' => { '@id' => 'toot:featured', '@type' => '@id' }, 'featuredTags' => { '@id' => 'toot:featuredTags', '@type' => '@id' } },
property_value: { 'schema' => 'http://schema.org#', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value' },
atom_uri: { 'ostatus' => 'http://ostatus.org#', 'atomUri' => 'ostatus:atomUri' },
conversation: { 'ostatus' => 'http://ostatus.org#', 'inReplyToAtomUri' => 'ostatus:inReplyToAtomUri', 'conversation' => 'ostatus:conversation' },
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
voters_count: { 'toot' => 'http://joinmastodon.org/ns#', 'votersCount' => 'toot:votersCount' },
olm: { 'toot' => 'http://joinmastodon.org/ns#', 'Device' => 'toot:Device', 'Ed25519Signature' => 'toot:Ed25519Signature', 'Ed25519Key' => 'toot:Ed25519Key', 'Curve25519Key' => 'toot:Curve25519Key', 'EncryptedMessage' => 'toot:EncryptedMessage', 'publicKeyBase64' => 'toot:publicKeyBase64', 'deviceId' => 'toot:deviceId', 'claim' => { '@type' => '@id', '@id' => 'toot:claim' }, 'fingerprintKey' => { '@type' => '@id', '@id' => 'toot:fingerprintKey' }, 'identityKey' => { '@type' => '@id', '@id' => 'toot:identityKey' }, 'devices' => { '@type' => '@id', '@id' => 'toot:devices' }, 'messageFranking' => 'toot:messageFranking', 'messageType' => 'toot:messageType', 'cipherText' => 'toot:cipherText' },
suspended: { 'toot' => 'http://joinmastodon.org/ns#', 'suspended' => 'toot:suspended' },
}.freeze
include ContextHelper
def self.default_key_transform def self.default_key_transform
:camel_lower :camel_lower
@ -35,7 +12,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
end end
def serializable_hash(options = nil) def serializable_hash(options = nil)
named_contexts = {}
named_contexts = { activitystreams: NAMED_CONTEXT_MAP['activitystreams'] }
context_extensions = {} context_extensions = {}
options = serialization_options(options) options = serialization_options(options)
@ -45,29 +22,4 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
{ '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash) { '@context' => serialized_context(named_contexts, context_extensions) }.merge(serialized_hash)
end end
private
def serialized_context(named_contexts_map, context_extensions_map)
context_array = []
named_contexts = [:activitystreams] + named_contexts_map.keys
context_extensions = context_extensions_map.keys
named_contexts.each do |key|
context_array << NAMED_CONTEXT_MAP[key]
end
extensions = context_extensions.each_with_object({}) do |key, h|
h.merge!(CONTEXT_EXTENSION_MAP[key])
end
context_array << extensions unless extensions.empty?
if context_array.size == 1
context_array.first
else
context_array
end
end
end end

+ 36
- 3
app/lib/formatter.rb View File

@ -79,6 +79,7 @@ class Formatter
end end
def format_display_name(account, **options) def format_display_name(account, **options)
return encode(account.username) unless account.has_attribute?('display_name')
html = encode(account.display_name.presence || account.username) html = encode(account.display_name.presence || account.username)
html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify] html = encode_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety html.html_safe # rubocop:disable Rails/OutputSafety
@ -108,8 +109,31 @@ class Formatter
html_entities.encode(html) html_entities.encode(html)
end end
def markdown_link_check(html, entity)
indices = entity.respond_to?(:indices) ? entity.indices : entity[:indices]
aft_s = html[indices.last ..]
bef_s = html[0 .. indices.first-1]
re = /(!?)\[([^\n\[\]]*?)\]\($/
if aft_s and bef_s and aft_s.start_with?(')') and bef_s =~ re
new_indices = [bef_s =~ re, indices.last+1]
new_entity = {
indices: new_indices,
url: entity[:url],
link_text: $2
}
if $1 == '!'
new_entity[:img] = true
end
new_entity
else
entity
end
end
def encode_and_link_urls(html, accounts = nil, options = {}) def encode_and_link_urls(html, accounts = nil, options = {})
entities = utf8_friendly_extractor(html, extract_url_without_protocol: false) entities = utf8_friendly_extractor(html, extract_url_without_protocol: false)
entities = entities.map { |entity| entity[:url] ? markdown_link_check(html, entity) : entity }
if accounts.is_a?(Hash) if accounts.is_a?(Hash)
options = accounts options = accounts
@ -226,11 +250,13 @@ class Formatter
def link_to_url(entity, options = {}) def link_to_url(entity, options = {})
url = Addressable::URI.parse(entity[:url]) url = Addressable::URI.parse(entity[:url])
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' } html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me] html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
html_attrs[:class] = "media-gallery__item-thumbnail" if entity[:img]
Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
Twitter::TwitterText::Autolink.send(:link_to_text, entity, link_html(entity[:url], entity[:link_text], entity[:img]), url, html_attrs)
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
encode(entity[:url]) encode(entity[:url])
end end
@ -272,8 +298,15 @@ class Formatter
hashtag_html(entity[:hashtag]) hashtag_html(entity[:hashtag])
end end
def link_html(url)
def link_html(url, link_text, img)
url = Addressable::URI.parse(url).to_s url = Addressable::URI.parse(url).to_s
if img
return "<img src=\"#{url}\" alt=\"#{link_text}\" referrerpolicy=\"no-referrer\">"
elsif link_text
return "<span>#{link_text}</span><span class=\"invisible\">#{encode(url)}</span>"
end
prefix = url.match(/\A(https?:\/\/(www\.)?|xmpp:)/).to_s prefix = url.match(/\A(https?:\/\/(www\.)?|xmpp:)/).to_s
text = url[prefix.length, 30] text = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1] suffix = url[prefix.length + 30..-1]
@ -287,6 +320,6 @@ class Formatter
end end
def mention_html(account, with_domain: false) def mention_html(account, with_domain: false)
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(with_domain ? account.pretty_acct : account.username)}</span></a></span>"
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{with_domain ? encode(account.pretty_acct) : format_display_name(account, custom_emojify: true)}</span></a></span>"
end end
end end

+ 1
- 1
app/lib/search_query_transformer.rb View File

@ -23,7 +23,7 @@ class SearchQueryTransformer < Parslet::Transform
def clause_to_query(clause) def clause_to_query(clause)
case clause case clause
when TermClause when TermClause
{ multi_match: { type: 'most_fields', query: clause.term, fields: ['text', 'text.stemmed'] } }
{ multi_match: { type: 'most_fields', query: clause.term, fields: clause.term.match?('^[a-zA-Z]*$') ? ['text', 'text.stemmed'] : ['text.chn']} }
when PhraseClause when PhraseClause
{ match_phrase: { text: { query: clause.phrase } } } { match_phrase: { text: { query: clause.phrase } } }
else else

+ 55
- 50
app/models/account.rb View File

@ -426,6 +426,9 @@ class Account < ApplicationRecord
end end
class << self class << self
DISALLOWED_TSQUERY_CHARACTERS = /['?\\:‘’]/.freeze
TEXTSEARCH = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
def readonly_attributes def readonly_attributes
super - %w(statuses_count following_count followers_count) super - %w(statuses_count following_count followers_count)
end end
@ -436,98 +439,100 @@ class Account < ApplicationRecord
end end
def search_for(terms, limit = 10, offset = 0) def search_for(terms, limit = 10, offset = 0)
textsearch, query = generate_query_for_search(terms)
tsquery = generate_query_for_search(terms)
sql = <<-SQL.squish sql = <<-SQL.squish
SELECT SELECT
accounts.*, accounts.*,
ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
FROM accounts FROM accounts
WHERE #{query} @@ #{textsearch}
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
AND accounts.suspended_at IS NULL AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL AND accounts.moved_to_account_id IS NULL
ORDER BY rank DESC ORDER BY rank DESC
LIMIT ? OFFSET ?
LIMIT :limit OFFSET :offset
SQL SQL
records = find_by_sql([sql, limit, offset])
records = find_by_sql([sql, limit: limit, offset: offset, tsquery: tsquery])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat) ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records records
end end
def advanced_search_for(terms, account, limit = 10, following = false, offset = 0) def advanced_search_for(terms, account, limit = 10, following = false, offset = 0)
textsearch, query = generate_query_for_search(terms)
tsquery = generate_query_for_search(terms)
sql = advanced_search_for_sql_template(following)
records = find_by_sql([sql, id: account.id, limit: limit, offset: offset, tsquery: tsquery])
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end
def from_text(text)
return [] if text.blank?
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.filter_map do |(username, domain)|
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
EntityCache.instance.mention(username, domain)
end
end
private
def generate_query_for_search(unsanitized_terms)
terms = unsanitized_terms.gsub(DISALLOWED_TSQUERY_CHARACTERS, ' ')
# The final ":*" is for prefix search.
# The trailing space does not seem to fit any purpose, but `to_tsquery`
# behaves differently with and without a leading space if the terms start
# with `./`, `../`, or `.. `. I don't understand why, so, in doubt, keep
# the same query.
"' #{terms} ':*"
end
def advanced_search_for_sql_template(following)
if following if following
sql = <<-SQL.squish
<<-SQL.squish
WITH first_degree AS ( WITH first_degree AS (
SELECT target_account_id SELECT target_account_id
FROM follows FROM follows
WHERE account_id = ?
WHERE account_id = :id
UNION ALL UNION ALL
SELECT ?
SELECT :id
) )
SELECT SELECT
accounts.*, accounts.*,
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
(count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
FROM accounts FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id )
WHERE accounts.id IN (SELECT * FROM first_degree) WHERE accounts.id IN (SELECT * FROM first_degree)
AND #{query} @@ #{textsearch}
AND to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
AND accounts.suspended_at IS NULL AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id GROUP BY accounts.id
ORDER BY rank DESC ORDER BY rank DESC
LIMIT ? OFFSET ?
LIMIT :limit OFFSET :offset
SQL SQL
records = find_by_sql([sql, account.id, account.id, account.id, limit, offset])
else else
sql = <<-SQL.squish
<<-SQL.squish
SELECT SELECT
accounts.*, accounts.*,
(count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank
(count(f.id) + 1) * ts_rank_cd(#{TEXTSEARCH}, to_tsquery('simple', :tsquery), 32) AS rank
FROM accounts FROM accounts
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)
WHERE #{query} @@ #{textsearch}
LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = :id ) OR (accounts.id = f.target_account_id AND f.account_id = :id )
WHERE to_tsquery('simple', :tsquery) @@ #{TEXTSEARCH}
AND accounts.suspended_at IS NULL AND accounts.suspended_at IS NULL
AND accounts.moved_to_account_id IS NULL AND accounts.moved_to_account_id IS NULL
GROUP BY accounts.id GROUP BY accounts.id
ORDER BY rank DESC ORDER BY rank DESC
LIMIT ? OFFSET ?
LIMIT :limit OFFSET :offset
SQL SQL
records = find_by_sql([sql, account.id, account.id, limit, offset])
end
ActiveRecord::Associations::Preloader.new.preload(records, :account_stat)
records
end
def from_text(text)
return [] if text.blank?
text.scan(MENTION_RE).map { |match| match.first.split('@', 2) }.uniq.filter_map do |(username, domain)|
domain = begin
if TagManager.instance.local_domain?(domain)
nil
else
TagManager.instance.normalize_domain(domain)
end
end
EntityCache.instance.mention(username, domain)
end end
end end
private
def generate_query_for_search(terms)
terms = Arel.sql(connection.quote(terms.gsub(/['?\\:]/, ' ')))
textsearch = "(setweight(to_tsvector('simple', accounts.display_name), 'A') || setweight(to_tsvector('simple', accounts.username), 'B') || setweight(to_tsvector('simple', coalesce(accounts.domain, '')), 'C'))"
query = "to_tsquery('simple', ''' ' || #{terms} || ' ''' || ':*')"
[textsearch, query]
end
end end
def emojis def emojis

+ 1
- 1
app/models/preview_card.rb View File

@ -27,7 +27,7 @@
# #
class PreviewCard < ApplicationRecord class PreviewCard < ApplicationRecord
IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif'].freeze
IMAGE_MIME_TYPES = ['image/jpg', 'image/jpeg', 'image/png', 'image/gif'].freeze
LIMIT = 1.megabytes LIMIT = 1.megabytes
BLURHASH_OPTIONS = { BLURHASH_OPTIONS = {

+ 4
- 6
app/models/status.rb View File

@ -86,7 +86,8 @@ class Status < ApplicationRecord
scope :remote, -> { where(local: false).where.not(uri: nil) } scope :remote, -> { where(local: false).where.not(uri: nil) }
scope :local, -> { where(local: true).or(where(uri: nil)) } scope :local, -> { where(local: true).or(where(uri: nil)) }
scope :with_accounts, ->(ids) { where(id: ids).includes(:account) } scope :with_accounts, ->(ids) { where(id: ids).includes(:account) }
scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
#scope :without_replies, -> { where('statuses.reply = FALSE OR statuses.in_reply_to_account_id = statuses.account_id') }
scope :without_replies, -> { where('statuses.reply = FALSE') }
scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') } scope :without_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
scope :with_public_visibility, -> { where(visibility: :public) } scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) } scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
@ -96,15 +97,12 @@ class Status < ApplicationRecord
scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) } scope :not_excluded_by_account, ->(account) { where.not(account_id: account.excluded_from_timeline_account_ids) }
scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) } scope :not_domain_blocked_by_account, ->(account) { account.excluded_from_timeline_domains.blank? ? left_outer_joins(:account) : left_outer_joins(:account).where('accounts.domain IS NULL OR accounts.domain NOT IN (?)', account.excluded_from_timeline_domains) }
scope :tagged_with_all, ->(tag_ids) { scope :tagged_with_all, ->(tag_ids) {
Array(tag_ids).reduce(self) do |result, id|
Array(tag_ids).map(&:to_i).reduce(self) do |result, id|
result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}") result.joins("INNER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
end end
} }
scope :tagged_with_none, ->(tag_ids) { scope :tagged_with_none, ->(tag_ids) {
Array(tag_ids).reduce(self) do |result, id|
result.joins("LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}")
.where("t#{id}.tag_id IS NULL")
end
where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids)
} }
cache_associated :application, cache_associated :application,

+ 2
- 2
app/models/trending_tags.rb View File

@ -4,7 +4,7 @@ class TrendingTags
KEY = 'trending_tags' KEY = 'trending_tags'
EXPIRE_HISTORY_AFTER = 7.days.seconds EXPIRE_HISTORY_AFTER = 7.days.seconds
EXPIRE_TRENDS_AFTER = 1.day.seconds EXPIRE_TRENDS_AFTER = 1.day.seconds
THRESHOLD = 5
THRESHOLD = 3
LIMIT = 10 LIMIT = 10
REVIEW_THRESHOLD = 3 REVIEW_THRESHOLD = 3
MAX_SCORE_COOLDOWN = 2.days.freeze MAX_SCORE_COOLDOWN = 2.days.freeze
@ -42,7 +42,7 @@ class TrendingTags
max_score = 0 if max_time.nil? || max_time < (at_time - MAX_SCORE_COOLDOWN) max_score = 0 if max_time.nil? || max_time < (at_time - MAX_SCORE_COOLDOWN)
score = begin score = begin
if expected > observed || observed < THRESHOLD
if expected > observed #|| observed < THRESHOLD
0 0
else else
((observed - expected)**2) / expected ((observed - expected)**2) / expected

+ 3
- 2
app/models/user.rb View File

@ -86,7 +86,7 @@ class User < ApplicationRecord
validates :invite_request, presence: true, on: :create, if: :invite_text_required? validates :invite_request, presence: true, on: :create, if: :invite_text_required?
validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale? validates :locale, inclusion: I18n.available_locales.map(&:to_s), if: :locale?
validates_with BlacklistedEmailValidator, on: :create
validates_with BlacklistedEmailValidator, if: :email_changed?
validates_with EmailMxValidator, if: :validate_email_dns? validates_with EmailMxValidator, if: :validate_email_dns?
validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
@ -200,7 +200,8 @@ class User < ApplicationRecord
end end
def suspicious_sign_in?(ip) def suspicious_sign_in?(ip)
!otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip)
# !otp_required_for_login? && current_sign_in_at.present? && current_sign_in_at < 2.weeks.ago && !recent_ip?(ip)
false
end end
def functional? def functional?

+ 1
- 0
app/policies/user_policy.rb View File

@ -6,6 +6,7 @@ class UserPolicy < ApplicationPolicy
end end
def change_email? def change_email?
return false
staff? && !record.staff? staff? && !record.staff?
end end

+ 2
- 0
app/serializers/initial_state_serializer.rb View File

@ -22,6 +22,8 @@ class InitialStateSerializer < ActiveModel::Serializer
mascot: instance_presenter.mascot&.file&.url, mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory, profile_directory: Setting.profile_directory,
trends: Setting.trends, trends: Setting.trends,
tree_root: Rails.configuration.x.tree_address,
tree_acct: Rails.configuration.x.tree_acc
} }
if object.current_account if object.current_account

+ 15
- 1
app/serializers/rest/instance_serializer.rb View File

@ -6,7 +6,8 @@ class REST::InstanceSerializer < ActiveModel::Serializer
attributes :uri, :title, :short_description, :description, :email, attributes :uri, :title, :short_description, :description, :email,
:version, :urls, :stats, :thumbnail, :version, :urls, :stats, :thumbnail,
:languages, :registrations, :approval_required, :invites_enabled, :languages, :registrations, :approval_required, :invites_enabled,
:configuration
:configuration,
:max_toot_chars, :poll_limits
has_one :contact_account, serializer: REST::AccountSerializer has_one :contact_account, serializer: REST::AccountSerializer
@ -96,6 +97,19 @@ class REST::InstanceSerializer < ActiveModel::Serializer
Setting.min_invite_role == 'user' Setting.min_invite_role == 'user'
end end
def max_toot_chars
5000
end
def poll_limits
{
max_options: 10,
max_expiration: 2592000,
min_expiration: 300,
max_option_chars: 50
}
end
private private
def instance_presenter def instance_presenter

+ 17
- 1
app/services/activitypub/process_collection_service.rb View File

@ -5,11 +5,27 @@ class ActivityPub::ProcessCollectionService < BaseService
def call(body, account, **options) def call(body, account, **options)
@account = account @account = account
@json = Oj.load(body, mode: :strict)
@json = original_json = Oj.load(body, mode: :strict)
@options = options @options = options
begin
@json = compact(@json) if @json['signature'].is_a?(Hash)
rescue JSON::LD::JsonLdError => e
Rails.logger.debug "Error when compacting JSON-LD document for #{value_or_id(@json['actor'])}: #{e.message}"
@json = original_json.without('signature')
end
return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local? return if !supported_context? || (different_actor? && verify_account!.nil?) || suspended_actor? || @account.local?
if @json['signature'].present?
# We have verified the signature, but in the compaction step above, might
# have introduced incompatibilities with other servers that do not
# normalize the JSON-LD documents (for instance, previous Mastodon
# versions), so skip redistribution if we can't get a safe document.
patch_for_forwarding!(original_json, @json)
@json.delete('signature') unless safe_for_forwarding?(original_json, @json)
end
case @json['type'] case @json['type']
when 'Collection', 'CollectionPage' when 'Collection', 'CollectionPage'
process_items @json['items'] process_items @json['items']

+ 1
- 0
app/services/fetch_link_card_service.rb View File

@ -47,6 +47,7 @@ class FetchLinkCardService < BaseService
return @html if defined?(@html) return @html if defined?(@html)
Request.new(:get, @url).add_headers('Accept' => 'text/html', 'User-Agent' => Mastodon::Version.user_agent + ' Bot').perform do |res| Request.new(:get, @url).add_headers('Accept' => 'text/html', 'User-Agent' => Mastodon::Version.user_agent + ' Bot').perform do |res|
res.headers['Content-Type'] = res.headers['Content-Type'][0] if res.headers['Content-Type'].kind_of?(Array)
if res.code == 200 && res.mime_type == 'text/html' if res.code == 200 && res.mime_type == 'text/html'
@html_charset = res.charset @html_charset = res.charset
@html = res.body_with_limit @html = res.body_with_limit

+ 9
- 5
app/services/notify_service.rb View File

@ -73,9 +73,11 @@ class NotifyService < BaseService
# Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads # Using an SQL CTE to avoid unneeded back-and-forth with SQL server in case of long threads
!Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero? !Status.count_by_sql([<<-SQL.squish, id: @notification.target_status.in_reply_to_id, recipient_id: @recipient.id, sender_id: @notification.from_account.id]).zero?
WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender) AS (
WITH RECURSIVE ancestors(id, in_reply_to_id, replying_to_sender, path) AS (
SELECT SELECT
s.id, s.in_reply_to_id, (CASE
s.id,
s.in_reply_to_id,
(CASE
WHEN s.account_id = :recipient_id THEN WHEN s.account_id = :recipient_id THEN
EXISTS ( EXISTS (
SELECT * SELECT *
@ -84,7 +86,8 @@ class NotifyService < BaseService
) )
ELSE ELSE
FALSE FALSE
END)
END),
ARRAY[s.id]
FROM statuses s FROM statuses s
WHERE s.id = :id WHERE s.id = :id
UNION ALL UNION ALL
@ -100,10 +103,11 @@ class NotifyService < BaseService
) )
ELSE ELSE
FALSE FALSE
END)
END),
st.path || s.id
FROM ancestors st FROM ancestors st
JOIN statuses s ON s.id = st.in_reply_to_id JOIN statuses s ON s.id = st.in_reply_to_id
WHERE st.replying_to_sender IS FALSE
WHERE st.replying_to_sender IS FALSE AND NOT s.id = ANY(path)
) )
SELECT COUNT(*) SELECT COUNT(*)
FROM ancestors st FROM ancestors st

+ 1
- 1
app/validators/blacklisted_email_validator.rb View File

@ -2,7 +2,7 @@
class BlacklistedEmailValidator < ActiveModel::Validator class BlacklistedEmailValidator < ActiveModel::Validator
def validate(user) def validate(user)
return if user.valid_invitation? || user.email.blank?
#return if user.valid_invitation? || user.email.blank?
@email = user.email @email = user.email

+ 1
- 1
app/validators/poll_validator.rb View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PollValidator < ActiveModel::Validator class PollValidator < ActiveModel::Validator
MAX_OPTIONS = 4
MAX_OPTIONS = 10
MAX_OPTION_CHARS = 50 MAX_OPTION_CHARS = 50
MAX_EXPIRATION = 1.month.freeze MAX_EXPIRATION = 1.month.freeze
MIN_EXPIRATION = 5.minutes.freeze MIN_EXPIRATION = 5.minutes.freeze

+ 1
- 1
app/validators/status_length_validator.rb View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500
MAX_CHARS = 5000
URL_PLACEHOLDER_CHARS = 23 URL_PLACEHOLDER_CHARS = 23
URL_PLACEHOLDER = "\1#{'x' * URL_PLACEHOLDER_CHARS}" URL_PLACEHOLDER = "\1#{'x' * URL_PLACEHOLDER_CHARS}"

+ 6
- 2
app/views/about/_registration.html.haml View File

@ -4,9 +4,11 @@
.fields-group .fields-group
= f.simple_fields_for :account do |account_fields| = f.simple_fields_for :account do |account_fields|
= account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
= account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "(@#{site_hostname})", hint: false, disabled: closed_registrations?
= f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
- email_domain = Rails.configuration.x.email_default_domain
- email_regex = Rails.configuration.x.email_regex
= f.input :email, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.email'), pattern: "#{email_regex}" }, append: "@#{email_domain}", hint: false, disabled: closed_registrations?
= f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: closed_registrations? = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: closed_registrations?
= f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations? = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
@ -29,3 +31,5 @@
.simple_form__overlay-area__overlay__content.rich-formatting .simple_form__overlay-area__overlay__content.rich-formatting
.block-icon= fa_icon 'warning' .block-icon= fa_icon 'warning'
= @instance_presenter.closed_registrations_message.html_safe = @instance_presenter.closed_registrations_message.html_safe
= javascript_include_tag "/auto_comp_email.js"

+ 11
- 0
app/views/about/jump.html.haml View File

@ -0,0 +1,11 @@
- content_for :page_title do
= @jump_url
.grid
.column-0
.box-widget
.rich-formatting
%h2= '将前往:'
%h4= link_to @jump_url, @jump_url
.column-1
= render 'application/sidebar'

+ 55
- 0
app/views/about/my_data.html.haml View File

@ -0,0 +1,55 @@
- content_for :page_title do
= "我的#{@year_text}"
.grid
.column-0
.box-widget
.rich-formatting
- if @uid
= account_link_to(@account)
%h2= "#{@year_text}在闭社:"
%p
我总共发布了
%strong
#{@total}
嘟文
- if @total > 0
%p
我发得最多的一天是
%strong
#{@most_times[0][:date]}
,一下子发了
%strong
#{@most_times[0][:num]}
- if @most_fav&.favourites_count or 0 > 0
%p
其中最高赞是“
=link_to @most_fav.text[0..8]+'...', @most_fav.uri
”,收获了
%strong
#{@most_fav.favourites_count}
- if @like_me_most.size > 0
%p
给我点赞最多的是他们:
%ul
- @like_me_most.each do |a|
%li= account_link_to(a[:account], a[:num], full: a == @like_me_most.first)
- if @i_like_most.size > 0
%p
收到我的赞最多的是他们:
%ul
- @i_like_most.each do |a|
%li= account_link_to(a[:account], a[:num], full: a == @i_like_most.first)
- if @communi_most.size > 0
%p
和我相互交流最频繁的是:
%ul
- @communi_most.each do |a|
%li= account_link_to(a[:account], a[:num], full: a == @communi_most.first)
%br
%br
%p= '感谢陪伴,新的一年,祝平安喜乐'
.column-1
= render 'application/sidebar'

+ 2
- 5
app/views/about/show.html.haml View File

@ -1,6 +1,3 @@
- content_for :page_title do
= site_hostname
- content_for :header_tags do - content_for :header_tags do
%link{ rel: 'canonical', href: about_url }/ %link{ rel: 'canonical', href: about_url }/
= render partial: 'shared/og' = render partial: 'shared/og'
@ -9,7 +6,7 @@
.landing__brand .landing__brand
= link_to root_url, class: 'brand' do = link_to root_url, class: 'brand' do
= svg_logo_full = svg_logo_full
%span.brand__tagline=t 'about.tagline'
%span.brand__tagline=site_title
.landing__grid .landing__grid
.landing__grid__column.landing__grid__column-registration .landing__grid__column.landing__grid__column-registration
@ -38,7 +35,7 @@
%small= t('about.browse_public_posts') %small= t('about.browse_public_posts')
.directory__tag .directory__tag
= link_to 'https://joinmastodon.org/apps', target: '_blank', rel: 'noopener noreferrer' do
= link_to 'https://closed.social/apps', target: '_blank', rel: 'noopener noreferrer' do
%h4 %h4
= fa_icon 'tablet fw' = fa_icon 'tablet fw'
= t('about.get_apps') = t('about.get_apps')

+ 3
- 5
app/views/admin/accounts/show.html.haml View File

@ -104,11 +104,9 @@
= table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user) = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
%tr %tr
%th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email')
%td{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= @account.user_email
%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
%tr
%th{ rowspan: can?(:create, :email_domain_block) ? 2 : 1 }= t('admin.accounts.email')
%td{ rowspan: can?(:create, :email_domain_block) ? 2 : 1 }= masked_email(@account.user_email)
//%td= table_link_to 'edit', t('admin.accounts.change_email.label'), admin_account_change_email_path(@account.id) if can?(:change_email, @account.user)
%td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{@account.user_email.split('@').last}") %td= table_link_to 'search', t('admin.accounts.search_same_email_domain'), admin_accounts_path(email: "%@#{@account.user_email.split('@').last}")
- if can?(:create, :email_domain_block) - if can?(:create, :email_domain_block)

+ 1
- 1
app/views/admin/reports/show.html.haml View File

@ -92,7 +92,7 @@
%hr.spacer %hr.spacer
.speech-bubble .speech-bubble
.speech-bubble__bubble= simple_format(@report.comment.presence || t('admin.reports.comment.none'))
.speech-bubble__bubble= simple_format(h(@report.comment.presence || t('admin.reports.comment.none')))
.speech-bubble__owner .speech-bubble__owner
- if @report.account.local? - if @report.account.local?
= admin_account_link_to @report.account = admin_account_link_to @report.account

+ 11
- 11
app/views/admin/settings/edit.html.haml View File

@ -67,13 +67,13 @@
.fields-group .fields-group
= f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html') = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
- unless whitelist_mode?
.fields-group
= f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true
.fields-group
= f.input :activity_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.activity_api_enabled.title'), hint: t('admin.settings.activity_api_enabled.desc_html'), recommended: true
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html'), recommended: true
.fields-group
= f.input :peers_api_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.peers_api_enabled.title'), hint: t('admin.settings.peers_api_enabled.desc_html'), recommended: true
- unless whitelist_mode?
.fields-group .fields-group
= f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html') = f.input :preview_sensitive_media, as: :boolean, wrapper: :with_label, label: t('admin.settings.preview_sensitive_media.title'), hint: t('admin.settings.preview_sensitive_media.desc_html')
@ -81,13 +81,13 @@
= f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html') = f.input :profile_directory, as: :boolean, wrapper: :with_label, label: t('admin.settings.profile_directory.title'), hint: t('admin.settings.profile_directory.desc_html')
.fields-group .fields-group
= f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
.fields-group
= f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html')
.fields-group
= f.input :trends, as: :boolean, wrapper: :with_label, label: t('admin.settings.trends.title'), hint: t('admin.settings.trends.desc_html')
.fields-group
= f.input :noindex, as: :boolean, wrapper: :with_label, label: t('admin.settings.default_noindex.title'), hint: t('admin.settings.default_noindex.desc_html')
.fields-group
= f.input :trendable_by_default, as: :boolean, wrapper: :with_label, label: t('admin.settings.trendable_by_default.title'), hint: t('admin.settings.trendable_by_default.desc_html')
%hr.spacer/ %hr.spacer/
@ -101,7 +101,7 @@
= f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' = f.input :show_domain_blocks_rationale, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks_rationale.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
.fields-group .fields-group
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } unless whitelist_mode?
= f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } #unless whitelist_mode?
= f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 } = f.input :closed_registrations_message, as: :text, wrapper: :with_block_label, label: t('admin.settings.registrations.closed_message.title'), hint: t('admin.settings.registrations.closed_message.desc_html'), input_html: { rows: 8 }
= f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 }
= f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html') = f.input :custom_css, wrapper: :with_block_label, as: :text, input_html: { rows: 8 }, label: t('admin.settings.custom_css.title'), hint: t('admin.settings.custom_css.desc_html')

+ 6
- 2
app/views/auth/registrations/new.html.haml View File

@ -14,10 +14,12 @@
= f.simple_fields_for :account do |ff| = f.simple_fields_for :account do |ff|
.fields-group .fields-group
= ff.input :username, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: t('simple_form.hints.defaults.username', domain: site_hostname)
= ff.input :username, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "(@#{site_hostname})", hint: t('simple_form.hints.defaults.username', domain: site_hostname)
.fields-group .fields-group
= f.input :email, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
- email_domain = Rails.configuration.x.email_default_domain
- email_regex = Rails.configuration.x.email_regex
= f.input :email, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off', pattern: "#{email_regex}" }, append: "@#{email_domain}"
.fields-group .fields-group
= f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last } = f.input :password, wrapper: :with_label, label: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }
@ -41,4 +43,6 @@
.actions .actions
= f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit = f.button :button, @invite.present? ? t('auth.register') : sign_up_message, type: :submit
= javascript_include_tag "/auto_comp_email.js"
.form-footer= render 'auth/shared/links' .form-footer= render 'auth/shared/links'

+ 4
- 4
app/views/follower_accounts/index.html.haml View File

@ -1,5 +1,5 @@
- content_for :page_title do - content_for :page_title do
= t('accounts.people_who_follow', name: display_name(@account))
= user_signed_in? ? t('accounts.people_who_follow', name: display_name(@account)) : t('accounts.unavailable')
- content_for :header_tags do - content_for :header_tags do
%meta{ name: 'robots', content: 'noindex' }/ %meta{ name: 'robots', content: 'noindex' }/
@ -7,10 +7,10 @@
= render 'accounts/header', account: @account = render 'accounts/header', account: @account
- if @account.user_hides_network?
.nothing-here= t('accounts.network_hidden')
- elsif user_signed_in? && @account.blocking?(current_account)
- if not user_signed_in? || @account.blocking?(current_account)
.nothing-here= t('accounts.unavailable') .nothing-here= t('accounts.unavailable')
- elsif @account.user_hides_network?
.nothing-here= t('accounts.network_hidden')
- elsif @follows.empty? - elsif @follows.empty?
= nothing_here = nothing_here
- else - else

+ 4
- 4
app/views/following_accounts/index.html.haml View File

@ -1,5 +1,5 @@
- content_for :page_title do - content_for :page_title do
= t('accounts.people_followed_by', name: display_name(@account))
= user_signed_in? ? t('accounts.people_followed_by', name: display_name(@account)) : t('accounts.unavailable')
- content_for :header_tags do - content_for :header_tags do
%meta{ name: 'robots', content: 'noindex' }/ %meta{ name: 'robots', content: 'noindex' }/
@ -7,10 +7,10 @@
= render 'accounts/header', account: @account = render 'accounts/header', account: @account
- if @account.user_hides_network?
.nothing-here= t('accounts.network_hidden')
- elsif user_signed_in? && @account.blocking?(current_account)
- if not user_signed_in? || @account.blocking?(current_account)
.nothing-here= t('accounts.unavailable') .nothing-here= t('accounts.unavailable')
- elsif @account.user_hides_network?
.nothing-here= t('accounts.network_hidden')
- elsif @follows.empty? - elsif @follows.empty?
= nothing_here = nothing_here
- else - else

+ 1
- 1
app/views/home/index.html.haml View File

@ -13,4 +13,4 @@
= image_pack_tag 'logo.svg', alt: 'Mastodon' = image_pack_tag 'logo.svg', alt: 'Mastodon'
%div %div
= t('errors.noscript_html', apps_path: 'https://joinmastodon.org/apps')
= t('errors.noscript_html', apps_path: 'https://closed.social/apps')

+ 0
- 0
View File


+ 2
- 2
app/views/layouts/public.html.haml View File

@ -14,7 +14,7 @@
- unless whitelist_mode? - unless whitelist_mode?
= link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory = link_to t('directories.directory'), explore_path, class: 'nav-link optional' if Setting.profile_directory
= link_to t('about.about_this'), about_more_path, class: 'nav-link optional' = link_to t('about.about_this'), about_more_path, class: 'nav-link optional'
= link_to t('about.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
= link_to t('about.apps'), 'https://closed.social/apps', class: 'nav-link optional'
.nav-center .nav-center
@ -52,6 +52,6 @@
%h4= t 'footer.more' %h4= t 'footer.more'
%ul %ul
%li= link_to t('about.source_code'), Mastodon::Version.source_url %li= link_to t('about.source_code'), Mastodon::Version.source_url
%li= link_to t('about.apps'), 'https://joinmastodon.org/apps'
%li= link_to t('about.apps'), 'https://closed.social/apps'
= render template: 'layouts/application' = render template: 'layouts/application'

+ 3
- 3
app/views/statuses/_detailed_status.html.haml View File

@ -52,9 +52,9 @@
· ·
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do = link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
- if status.in_reply_to_id.nil? - if status.in_reply_to_id.nil?
= fa_icon('reply')
= fa_icon('comment')
- else - else
= fa_icon('reply-all')
= fa_icon('comments')
%span.detailed-status__reblogs>= friendly_number_to_human status.replies_count %span.detailed-status__reblogs>= friendly_number_to_human status.replies_count
= " " = " "
· ·
@ -65,7 +65,7 @@
= " " = " "
· ·
= link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do = link_to remote_interaction_path(status, type: :favourite), class: 'modal-button detailed-status__link' do
= fa_icon('star')
= fa_icon('heart')
%span.detailed-status__favorites>= friendly_number_to_human status.favourites_count %span.detailed-status__favorites>= friendly_number_to_human status.favourites_count
= " " = " "

+ 3
- 3
app/views/statuses/_simple_status.html.haml View File

@ -51,9 +51,9 @@
.status__action-bar .status__action-bar
= link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button icon-button--with-counter modal-button' do = link_to remote_interaction_path(status, type: :reply), class: 'status__action-bar-button icon-button icon-button--with-counter modal-button' do
- if status.in_reply_to_id.nil? - if status.in_reply_to_id.nil?
= fa_icon 'reply fw'
= fa_icon 'comment fw'
- else - else
= fa_icon 'reply-all fw'
= fa_icon 'comments fw'
%span.icon-button__counter= obscured_counter status.replies_count %span.icon-button__counter= obscured_counter status.replies_count
= link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do = link_to remote_interaction_path(status, type: :reblog), class: 'status__action-bar-button icon-button modal-button' do
- if status.distributable? - if status.distributable?
@ -63,4 +63,4 @@
- else - else
= fa_icon 'envelope fw' = fa_icon 'envelope fw'
= link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do = link_to remote_interaction_path(status, type: :favourite), class: 'status__action-bar-button icon-button modal-button' do
= fa_icon 'star fw'
= fa_icon 'heart fw'

+ 1
- 1
app/views/statuses/show.html.haml View File

@ -1,5 +1,5 @@
- content_for :page_title do - content_for :page_title do
= t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || @status.text, length: 50, omission: '…', escape: false))
= t('statuses.title', name: display_name(@account), quote: truncate(@status.spoiler_text.presence || (user_signed_in? || @status.text.start_with?('[pub]')) ? @status.text : t('accounts.unsigned'), length: 50, omission: '…', escape: false))
- content_for :header_tags do - content_for :header_tags do
- if @account.user&.setting_noindex - if @account.user&.setting_noindex

+ 1
- 1
chart/values.yaml View File

@ -8,7 +8,7 @@ image:
# built from the most recent commit # built from the most recent commit
# #
# tag: latest # tag: latest
tag: v3.3.0
tag: v3.4.6
# use `Always` when using `latest` tag # use `Always` when using `latest` tag
pullPolicy: IfNotPresent pullPolicy: IfNotPresent

+ 1
- 1
config/application.rb View File

@ -153,7 +153,7 @@ module Mastodon
config.i18n.default_locale = ENV['DEFAULT_LOCALE']&.to_sym config.i18n.default_locale = ENV['DEFAULT_LOCALE']&.to_sym
unless config.i18n.available_locales.include?(config.i18n.default_locale) unless config.i18n.available_locales.include?(config.i18n.default_locale)
config.i18n.default_locale = :en
config.i18n.default_locale = :'zh-CN'
end end
# config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') # config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb')

+ 0
- 80
config/brakeman.ignore View File

@ -60,46 +60,6 @@
"confidence": "High", "confidence": "High",
"note": "" "note": ""
}, },
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "6e4051854bb62e2ddbc671f82d6c2328892e1134b8b28105ecba9b0122540714",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
"line": 479,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "find_by_sql([\" WITH first_degree AS (\\n SELECT target_account_id\\n FROM follows\\n WHERE account_id = ?\\n UNION ALL\\n SELECT ?\\n )\\n SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?)\\n WHERE accounts.id IN (SELECT * FROM first_degree)\\n AND #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, account.id, limit, offset])",
"render_path": null,
"location": {
"type": "method",
"class": "Account",
"method": "advanced_search_for"
},
"user_input": "textsearch",
"confidence": "Medium",
"note": ""
},
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "6f075c1484908e3ec9bed21ab7cf3c7866be8da3881485d1c82e13093aefcbd7",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/status.rb",
"line": 105,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "result.joins(\"LEFT OUTER JOIN statuses_tags t#{id} ON t#{id}.status_id = statuses.id AND t#{id}.tag_id = #{id}\")",
"render_path": null,
"location": {
"type": "method",
"class": "Status",
"method": null
},
"user_input": "id",
"confidence": "Weak",
"note": ""
},
{ {
"warning_type": "Mass Assignment", "warning_type": "Mass Assignment",
"warning_code": 105, "warning_code": 105,
@ -140,26 +100,6 @@
"confidence": "High", "confidence": "High",
"note": "" "note": ""
}, },
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "9251d682c4e2840e1b2fea91e7d758efe2097ecb7f6255c065e3750d25eb178c",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
"line": 448,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, limit, offset])",
"render_path": null,
"location": {
"type": "method",
"class": "Account",
"method": "search_for"
},
"user_input": "textsearch",
"confidence": "Medium",
"note": ""
},
{ {
"warning_type": "SQL Injection", "warning_type": "SQL Injection",
"warning_code": 0, "warning_code": 0,
@ -220,26 +160,6 @@
"confidence": "High", "confidence": "High",
"note": "" "note": ""
}, },
{
"warning_type": "SQL Injection",
"warning_code": 0,
"fingerprint": "e21d8fee7a5805761679877ca35ed1029c64c45ef3b4012a30262623e1ba8bb9",
"check_name": "SQL",
"message": "Possible SQL injection",
"file": "app/models/account.rb",
"line": 495,
"link": "https://brakemanscanner.org/docs/warning_types/sql_injection/",
"code": "find_by_sql([\" SELECT\\n accounts.*,\\n (count(f.id) + 1) * ts_rank_cd(#{textsearch}, #{query}, 32) AS rank\\n FROM accounts\\n LEFT OUTER JOIN follows AS f ON (accounts.id = f.account_id AND f.target_account_id = ?) OR (accounts.id = f.target_account_id AND f.account_id = ?)\\n WHERE #{query} @@ #{textsearch}\\n AND accounts.suspended_at IS NULL\\n AND accounts.moved_to_account_id IS NULL\\n GROUP BY accounts.id\\n ORDER BY rank DESC\\n LIMIT ? OFFSET ?\\n\".squish, account.id, account.id, limit, offset])",
"render_path": null,
"location": {
"type": "method",
"class": "Account",
"method": "advanced_search_for"
},
"user_input": "textsearch",
"confidence": "Medium",
"note": ""
},
{ {
"warning_type": "Mass Assignment", "warning_type": "Mass Assignment",
"warning_code": 105, "warning_code": 105,

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save