Compare commits

...

Author SHA1 Message Date
  zyn on thu.closed.social 966d47a536 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
101 changed files with 1922 additions and 193 deletions
Split View
  1. +2
    -0
      .gitignore
  2. +30
    -0
      CS_CONF.md
  3. +1
    -0
      app/chewy/statuses_index.rb
  4. +35
    -1
      app/controllers/about_controller.rb
  5. +2
    -2
      app/controllers/api/v1/instances/activity_controller.rb
  6. +2
    -2
      app/controllers/api/v1/instances/peers_controller.rb
  7. +1
    -1
      app/controllers/api/v1/instances_controller.rb
  8. +17
    -3
      app/controllers/api/v1/statuses_controller.rb
  9. +1
    -1
      app/controllers/home_controller.rb
  10. +2
    -1
      app/helpers/application_helper.rb
  11. +10
    -2
      app/helpers/home_helper.rb
  12. +1
    -1
      app/javascript/images/logo.svg
  13. +1
    -1
      app/javascript/images/logo_alt.svg
  14. +5
    -1
      app/javascript/images/logo_full.svg
  15. +1
    -1
      app/javascript/images/logo_transparent.svg
  16. +1
    -1
      app/javascript/images/logo_transparent_black.svg
  17. BIN
     
  18. BIN
     
  19. +8
    -2
      app/javascript/mastodon/actions/interactions.js
  20. +4
    -1
      app/javascript/mastodon/actions/timelines.js
  21. +12
    -1
      app/javascript/mastodon/components/media_gallery.js
  22. +80
    -4
      app/javascript/mastodon/components/status.js
  23. +473
    -0
      app/javascript/mastodon/components/status2.js
  24. +7
    -6
      app/javascript/mastodon/components/status_action_bar.js
  25. +1
    -1
      app/javascript/mastodon/components/status_content.js
  26. +64
    -4
      app/javascript/mastodon/containers/status_container.js
  27. +168
    -0
      app/javascript/mastodon/containers/status_container2.js
  28. +2
    -2
      app/javascript/mastodon/features/compose/components/compose_form.js
  29. +16
    -1
      app/javascript/mastodon/features/compose/index.js
  30. +1
    -1
      app/javascript/mastodon/features/favourited_statuses/index.js
  31. +1
    -1
      app/javascript/mastodon/features/getting_started/components/trends.js
  32. +1
    -1
      app/javascript/mastodon/features/getting_started/index.js
  33. +2
    -2
      app/javascript/mastodon/features/notifications/components/filter_bar.js
  34. +1
    -1
      app/javascript/mastodon/features/notifications/components/notification.js
  35. +6
    -5
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  36. +9
    -6
      app/javascript/mastodon/features/status/components/action_bar.js
  37. +18
    -5
      app/javascript/mastodon/features/status/components/detailed_status.js
  38. +94
    -11
      app/javascript/mastodon/features/status/index.js
  39. +6
    -0
      app/javascript/mastodon/features/ui/components/columns_area.js
  40. +1
    -1
      app/javascript/mastodon/features/ui/components/link_footer.js
  41. +3
    -2
      app/javascript/mastodon/features/ui/components/navigation_panel.js
  42. +13
    -0
      app/javascript/mastodon/features/ui/components/tabs_bar.js
  43. +3
    -0
      app/javascript/mastodon/initial_state.js
  44. +5
    -2
      app/javascript/mastodon/locales/en.json
  45. +5
    -2
      app/javascript/mastodon/locales/zh-CN.json
  46. +11
    -8
      app/javascript/mastodon/locales/zh-HK.json
  47. +14
    -10
      app/javascript/mastodon/locales/zh-TW.json
  48. +4
    -0
      app/javascript/styles/application.scss
  49. +115
    -0
      app/javascript/styles/closed-social/global.scss
  50. +39
    -0
      app/javascript/styles/closed-social/timeline_comments.scss
  51. +65
    -0
      app/javascript/styles/closed-social/tree.scss
  52. +1
    -0
      app/javascript/styles/mastodon-light/variables.scss
  53. +2
    -1
      app/javascript/styles/mastodon/about.scss
  54. +1
    -1
      app/javascript/styles/mastodon/forms.scss
  55. +4
    -1
      app/javascript/styles/mastodon/variables.scss
  56. +3
    -0
      app/javascript/styles/thu.scss
  57. +221
    -0
      app/javascript/styles/thu/diff.scss
  58. +8
    -0
      app/javascript/styles/thu/variables.scss
  59. +36
    -3
      app/lib/formatter.rb
  60. +1
    -1
      app/lib/search_query_transformer.rb
  61. +1
    -1
      app/models/preview_card.rb
  62. +5
    -2
      app/models/status.rb
  63. +2
    -2
      app/models/trending_tags.rb
  64. +1
    -1
      app/models/user.rb
  65. +2
    -0
      app/serializers/initial_state_serializer.rb
  66. +1
    -0
      app/services/fetch_link_card_service.rb
  67. +1
    -1
      app/validators/blacklisted_email_validator.rb
  68. +1
    -1
      app/validators/status_length_validator.rb
  69. +6
    -2
      app/views/about/_registration.html.haml
  70. +11
    -0
      app/views/about/jump.html.haml
  71. +55
    -0
      app/views/about/my_data.html.haml
  72. +2
    -5
      app/views/about/show.html.haml
  73. +11
    -11
      app/views/admin/settings/edit.html.haml
  74. +6
    -2
      app/views/auth/registrations/new.html.haml
  75. +4
    -4
      app/views/follower_accounts/index.html.haml
  76. +4
    -4
      app/views/following_accounts/index.html.haml
  77. +1
    -1
      app/views/home/index.html.haml
  78. +2
    -2
      app/views/layouts/public.html.haml
  79. +3
    -3
      app/views/statuses/_detailed_status.html.haml
  80. +3
    -3
      app/views/statuses/_simple_status.html.haml
  81. +1
    -1
      app/views/statuses/show.html.haml
  82. +1
    -1
      config/application.rb
  83. +2
    -2
      config/environments/production.rb
  84. +1
    -1
      config/initializers/content_security_policy.rb
  85. +13
    -0
      config/initializers/new_features.rb
  86. +8
    -8
      config/locales/devise.zh-CN.yml
  87. +19
    -18
      config/locales/zh-CN.yml
  88. +4
    -0
      config/routes.rb
  89. +1
    -0
      config/themes.yml
  90. +28
    -4
      db/schema.rb
  91. +2
    -2
      lib/mastodon/version.rb
  92. +2
    -0
      package.json
  93. BIN
     
  94. BIN
     
  95. +14
    -0
      public/auto_comp_email.js
  96. BIN
     
  97. BIN
     
  98. BIN
     
  99. +1
    -1
      public/mask-icon.svg
  100. BIN
     

+ 2
- 0
.gitignore View File

@ -4,6 +4,8 @@
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
*.orig
# Ignore bundler config and downloaded libraries.
/.bundle
/vendor/bundle

+ 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 :stemmed, type: 'text', analyzer: 'content'
field :chn , type: 'text', analyzer: 'ik_max_word', search_analyzer: 'ik_smart'
end
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'
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_instance_presenter
before_action :set_expires_in, only: [:more, :terms]
before_action :set_registration_form_time, only: :show
before_action :authenticate_user!, only: [:jump, :my_data]
skip_before_action :require_functional!, only: [:more, :terms]
@ -27,6 +28,39 @@ class AboutController < ApplicationController
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_rationale?
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!
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
expires_in 1.day, public: true
@ -33,6 +33,6 @@ class Api::V1::Instances::ActivityController < Api::BaseController
end
def require_enabled_api!
head 404 unless Setting.activity_api_enabled && !whitelist_mode?
head 404 unless Setting.activity_api_enabled #&& !whitelist_mode?
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!
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
expires_in 1.day, public: true
@ -14,6 +14,6 @@ class Api::V1::Instances::PeersController < Api::BaseController
private
def require_enabled_api!
head 404 unless Setting.peers_api_enabled && !whitelist_mode?
head 404 unless Setting.peers_api_enabled #&& !whitelist_mode?
end
end

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

@ -2,7 +2,7 @@
class Api::V1::InstancesController < Api::BaseController
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
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
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_descendants = cache_collection(descendants_results, Status)
@ -35,8 +39,13 @@ class Api::V1::StatusesController < Api::BaseController
end
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].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,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
@ -98,4 +107,9 @@ class Api::V1::StatusesController < Api::BaseController
def pagination_params(core_params)
params.slice(:limit).permit(:limit).merge(core_params)
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

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

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

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

@ -40,7 +40,8 @@ module ApplicationHelper
def available_sign_up_path
if closed_registrations?
'https://joinmastodon.org/#getting-started'
#'https://joinmastodon.org/#getting-started'
'https://closed.social'
else
new_user_registration_path
end

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

@ -7,7 +7,7 @@ module HomeHelper
}
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__wrapper') do
section = if account.nil?
@ -20,7 +20,7 @@ module HomeHelper
content_tag(:span, t('about.contact_unavailable'), class: 'display-name__account')
end
end
else
elsif full
link_to(path || ActivityPub::TagManager.instance.url_for(account), class: 'account__display-name') 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')
@ -32,6 +32,14 @@ module HomeHelper
content_tag(:span, "@#{account.acct}", class: 'display-name__account')
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
section + button

+ 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) => {
dispatch(unreblogRequest(status));
let old_reblogs_count = status.get('reblogs_count');
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));
}).catch(error => {
dispatch(unreblogFail(status, error));
@ -136,8 +139,11 @@ export function unfavourite(status) {
return (dispatch, getState) => {
dispatch(unfavouriteRequest(status));
let old_favourites_count = status.get('favourites_count');
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));
}).catch(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 compareId from 'mastodon/compare_id';
import { usePendingItems as preferPendingItems } from 'mastodon/initial_state';
import { fetchContext } from './statuses';
export const TIMELINE_UPDATE = 'TIMELINE_UPDATE';
export const TIMELINE_DELETE = 'TIMELINE_DELETE';
export const TIMELINE_CLEAR = 'TIMELINE_CLEAR';
@ -103,6 +103,9 @@ export function expandTimeline(timelineId, path, params = {}, done = noOp) {
const next = getLinks(response).refs.find(link => link.rel === 'next');
dispatch(importFetchedStatuses(response.data));
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') {
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 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
className='media-gallery__item-thumbnail'
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 Immutable from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
@ -10,7 +11,7 @@ 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, defineMessages, FormattedMessage } from 'react-intl';
import { injectIntl, defineMessages, FormattedMessage, FormattedNumber } 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';
@ -19,6 +20,8 @@ import Icon from 'mastodon/components/icon';
import { displayMedia } from '../initial_state';
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
// to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle';
@ -101,6 +104,7 @@ class Status extends ImmutablePureComponent {
inUse: PropTypes.bool,
available: PropTypes.bool,
}),
sonsIds: ImmutablePropTypes.list,
};
// Avoid checking props that are functions (and whose equality will always
@ -112,6 +116,8 @@ class Status extends ImmutablePureComponent {
'hidden',
'unread',
'pictureInPicture',
'sonsIds',
'ancestorsText',
];
state = {
@ -279,11 +285,40 @@ class Status extends ImmutablePureComponent {
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 () {
let media = null;
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;
@ -459,9 +494,44 @@ class Status extends ImmutablePureComponent {
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 (
<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}
<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>
</div>
{deepRec}
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} showThread={showThread} onExpandedToggle={this.handleExpandedToggle} collapsable onCollapsedToggle={this.handleCollapsedToggle} />
{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>
{sons}
</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}' },
block: { id: 'account.block', defaultMessage: 'Block @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
share: { id: 'status.share', defaultMessage: 'Share' },
more: { id: 'status.more', defaultMessage: 'More' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
@ -293,11 +294,11 @@ class StatusActionBar extends ImmutablePureComponent {
let replyIcon;
let replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else {
replyIcon = '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';
@ -319,9 +320,9 @@ class StatusActionBar extends ImmutablePureComponent {
return (
<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}

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

@ -181,7 +181,7 @@ export default class StatusContent extends React.PureComponent {
const hidden = this.props.onExpandedToggle ? !this.props.expanded : this.state.hidden;
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 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 { connect } from 'react-redux';
import Immutable from 'immutable';
import { createSelector } from 'reselect';
import Status from '../components/status';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import {
@ -39,7 +41,7 @@ import { initReport } from '../actions/reports';
import { openModal } from '../actions/modal';
import { deployPictureInPicture } from '../actions/picture_in_picture';
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';
const messages = defineMessages({
@ -56,11 +58,69 @@ const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
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 && ancestorsIds.first() == treeRoot.split('/').pop()) {
ancestorsText = getAncestorsText(state, { ids: ancestorsIds.shift() });
}
}
}
return {
status,
ancestorsText,
sonsIds,
pictureInPicture: getPictureInPicture(state, props),
};
};
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 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 = () => {
@ -249,7 +249,7 @@ class ComposeForm extends ImmutablePureComponent {
<PrivacyDropdownContainer />
<SpoilerButtonContainer />
</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 className='compose-form__publish'>

+ 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 { openModal } from 'mastodon/actions/modal';
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 { logOut } from 'mastodon/utils/log_out';
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
home_timeline: { id: 'tabs_bar.home', defaultMessage: 'Home' },
tree: {id: 'tabs_bar.tree', defaultMessage: 'Tree'},
notifications: { id: 'tabs_bar.notifications', defaultMessage: 'Notifications' },
public: { id: 'navigation_bar.public_timeline', defaultMessage: 'Federated timeline' },
community: { id: 'navigation_bar.community_timeline', defaultMessage: 'Local timeline' },
@ -49,6 +52,10 @@ class Compose extends React.PureComponent {
intl: PropTypes.object.isRequired,
};
state = {
showPinned: true,
}
componentDidMount () {
const { isSearchPage } = this.props;
@ -87,9 +94,14 @@ class Compose extends React.PureComponent {
onBlur = () => {
this.props.dispatch(changeComposing(false));
}
handleClear = () => {
this.setState({ showPinned: false});
}
render () {
const { multiColumn, showSearch, isSearchPage, intl } = this.props;
const { showPinned } = this.state;
let header = '';
@ -101,6 +113,9 @@ class Compose extends React.PureComponent {
{!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>
)}
{!columns.some(column => column.get('id') === 'TREE') && (
<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') && (
<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

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

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

@ -135,7 +135,7 @@ class GettingStarted extends ImmutablePureComponent {
navItems.push(
<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='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' />,
);

+ 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')}
title={intl.formatMessage(tooltips.mentions)}
>
<Icon id='reply-all' fixedWidth />
<Icon id='comments' fixedWidth />
</button>
<button
className={selectedFilter === 'favourite' ? 'active' : ''}
onClick={this.onClick('favourite')}
title={intl.formatMessage(tooltips.favourites)}
>
<Icon id='star' fixedWidth />
<Icon id='heart' fixedWidth />
</button>
<button
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='notification__message'>
<div className='notification__favourite-icon-wrapper'>
<Icon id='star' className='star-icon' fixedWidth />
<Icon id='heart' className='star-icon' fixedWidth />
</div>
<span title={notification.get('created_at')}>

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

@ -15,6 +15,7 @@ import { openModal } from 'mastodon/actions/modal';
const messages = defineMessages({
reply: { id: 'status.reply', defaultMessage: 'Reply' },
replyAll: { id: 'status.replyAll', defaultMessage: 'Reply to thread' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@ -127,11 +128,11 @@ class Footer extends ImmutablePureComponent {
let replyIcon, replyTitle;
if (status.get('in_reply_to_id', null) === null) {
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else {
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
} else {
replyIcon = 'reply-all';
replyTitle = intl.formatMessage(messages.replyAll);
}
let reblogTitle = '';
@ -148,9 +149,9 @@ class Footer extends ImmutablePureComponent {
return (
<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='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} />}
</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}' },
mention: { id: 'status.mention', defaultMessage: 'Mention @{name}' },
reply: { id: 'status.reply', defaultMessage: 'Reply' },
comment: {id: 'status.comment', defaultMessage: 'Comment' },
reblog: { id: 'status.reblog', defaultMessage: 'Boost' },
reblog_private: { id: 'status.reblog_private', defaultMessage: 'Boost with original visibility' },
cancel_reblog_private: { id: 'status.cancel_reblog_private', defaultMessage: 'Unboost' },
@ -251,11 +252,13 @@ class ActionBar extends React.PureComponent {
<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) {
replyIcon = 'reply';
replyIcon = 'comment';
replyTitle = intl.formatMessage(messages.comment);
} else {
replyIcon = 'reply-all';
replyIcon = 'reply';
replyTitle = intl.formatMessage(messages.reply);
}
const reblogPrivate = status.getIn(['account', 'id']) === me && status.get('visibility') === 'private';
@ -273,9 +276,9 @@ class ActionBar extends React.PureComponent {
return (
<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}
<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 MediaGallery from '../../../components/media_gallery';
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 ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
@ -105,7 +105,7 @@ class DetailedStatus extends ImmutablePureComponent {
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;
const outerStyle = { boxSizing: 'border-box' };
const { intl, compact, pictureInPicture } = this.props;
const { intl, compact, pictureInPicture, deep } = this.props;
if (!status) {
return null;
@ -220,7 +220,7 @@ class DetailedStatus extends ImmutablePureComponent {
if (this.context.router) {
favouriteLink = (
<Link to={`/statuses/${status.get('id')}/favourites`} className='detailed-status__link'>
<Icon id='star' />
<Icon id='heart' />
<span className='detailed-status__favorites'>
<AnimatedNumber value={status.get('favourites_count')} />
</span>
@ -229,7 +229,7 @@ class DetailedStatus extends ImmutablePureComponent {
} else {
favouriteLink = (
<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'>
<AnimatedNumber value={status.get('favourites_count')} />
</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 (
<div style={outerStyle}>
<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} />
</a>
{deepRec}
<StatusContent status={status} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
{media}
<div className='detailed-status__meta'>
<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}
</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 ActionBar from './components/action_bar';
import Column from '../ui/components/column';
import Tree from 'react-tree-graph';
import {
favourite,
unfavourite,
@ -52,7 +53,7 @@ import { openModal } from '../../actions/modal';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import ImmutablePureComponent from 'react-immutable-pure-component';
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 { textForScreenReader, defaultMediaVisibility } from '../../components/status';
import Icon from 'mastodon/components/icon';
@ -127,22 +128,60 @@ const makeMapStateToProps = () => {
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 status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
let rootAcct;
let deep;
let treeData;
if (status) {
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 {
status,
deep,
ancestorsIds,
descendantsIds,
treeData,
askReplyConfirmation: state.getIn(['compose', 'text']).trim().length !== 0,
domain: state.getIn(['meta', 'domain']),
pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }),
@ -180,6 +219,9 @@ class Status extends ImmutablePureComponent {
fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
loadedStatusId: undefined,
showTree: false,
svgWidth: 400,
activeNode: null
};
componentWillMount () {
@ -333,6 +375,13 @@ class Status extends ImmutablePureComponent {
}
}
handleShowTree = () => {
this.setState({
activeNode: null,
showTree: !this.state.showTree
});
}
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
@ -454,14 +503,17 @@ class Status extends ImmutablePureComponent {
}
}
renderChildren (list) {
return list.map(id => (
renderChildren (list, type) {
const { deep } = this.props;
return list.map((id,idx) => (
<StatusContainer
key={id}
id={id}
onMoveUp={this.handleMoveUp}
onMoveDown={this.handleMoveDown}
contextType='thread'
deep={deep==null? null : (type == 'ance'? idx : deep+1)}
tree_type={deep==null? null : type}
/>
));
}
@ -495,10 +547,19 @@ class Status extends ImmutablePureComponent {
this.setState({ fullscreen: isFullscreen() });
}
handleNodeClick = (ev, node) => {
if (!this.context.router) {
return;
}
const { status } = this.props;
this.context.router.history.push(`/statuses/${node}`);
}
render () {
let ancestors, descendants;
const { shouldUpdateScroll, status, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen } = this.state;
const { shouldUpdateScroll, status, deep, ancestorsIds, descendantsIds, treeData, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen, showTree, svgWidth, activeNode } = this.state;
if (status === null) {
return (
@ -510,11 +571,11 @@ class Status extends ImmutablePureComponent {
}
if (ancestorsIds && ancestorsIds.size > 0) {
ancestors = <div>{this.renderChildren(ancestorsIds)}</div>;
ancestors = <div>{this.renderChildren(ancestorsIds, 'ance')}</div>;
}
if (descendantsIds && descendantsIds.size > 0) {
descendants = <div>{this.renderChildren(descendantsIds)}</div>;
descendants = <div>{this.renderChildren(descendantsIds, 'desc')}</div>;
}
const handlers = {
@ -536,19 +597,40 @@ class Status extends ImmutablePureComponent {
showBackButton
multiColumn={multiColumn}
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' shouldUpdateScroll={shouldUpdateScroll}>
<div className={classNames('scrollable', { fullscreen })} ref={this.setRef}>
<div className={classNames('scrollable', { fullscreen }, {'tree':deep!=null})} ref={this.setRef}>
{ancestors}
<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
key={`details-${status.get('id')}`}
status={status}
deep={deep}
onOpenVideo={this.handleOpenVideo}
onOpenMedia={this.handleOpenMedia}
onToggleHidden={this.handleToggleHidden}
@ -557,6 +639,7 @@ class Status extends ImmutablePureComponent {
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
/>
}
<ActionBar
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 { scrollRight } from '../../../scroll';
import TrendsContainer from '../../getting_started/containers/trends_container';
const componentMap = {
'COMPOSE': Compose,
'HOME': HomeTimeline,
@ -171,6 +173,10 @@ class ColumnsArea extends ImmutablePureComponent {
return (
<div className='columns-area columns-area--mobile' key={index}>
{
['column.community', 'column.public'].includes(link.props['data-preview-title-id']) &&
<TrendsContainer />
}
{view}
</div>
);

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

@ -51,7 +51,7 @@ class LinkFooter extends React.PureComponent {
{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='/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='/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>

+ 3
- 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 { FormattedMessage } from 'react-intl';
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 FollowRequestsNavLink from './follow_requests_nav_link';
import ListPanel from './list_panel';
@ -11,12 +11,13 @@ import TrendsContainer from 'mastodon/features/getting_started/containers/trends
const NavigationPanel = () => (
<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={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>
<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' 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='/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='/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>}

+ 13
- 0
app/javascript/mastodon/features/ui/components/tabs_bar.js View File

@ -7,8 +7,12 @@ import { isUserTouching } from '../../../is_mobile';
import Icon from 'mastodon/components/icon';
import NotificationsCounterIcon from './notifications_counter_icon';
import { treeRoot } from '../../../initial_state';
import ReactHtmlParser, { processNodes, convertNodeToElement, htmlparser2 } from 'react-html-parser';
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={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='/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>,
@ -33,6 +37,10 @@ class TabsBar extends React.PureComponent {
history: PropTypes.object.isRequired,
}
state = {
showPinned: true,
}
setRef = ref => {
this.node = ref;
}
@ -69,8 +77,13 @@ class TabsBar extends React.PureComponent {
}
handleClear = () => {
this.setState({ showPinned: false});
}
render () {
const { intl: { formatMessage } } = this.props;
const { showPinned } = this.state;
return (
<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 disableSwiping = getMeta('disable_swiping');
export const treeRoot = getMeta('tree_root');
export const treeAcct = getMeta('tree_acct')
export default initialState;

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

@ -68,6 +68,7 @@
"column.favourites": "Favourites",
"column.follow_requests": "Follow requests",
"column.home": "Home",
"column.tree": "Tree",
"column.lists": "Lists",
"column.mutes": "Muted users",
"column.notifications": "Notifications",
@ -180,10 +181,10 @@
"generic.saved": "Saved",
"getting_started.developers": "Developers",
"getting_started.directory": "Profile directory",
"getting_started.documentation": "Documentation",
"getting_started.documentation": "Mastodon Documentation",
"getting_started.heading": "Getting started",
"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.terms": "Terms of service",
"hashtag.column_header.tag_mode.all": "and {additional}",
@ -395,6 +396,7 @@
"status.bookmark": "Bookmark",
"status.cancel_reblog_private": "Unboost",
"status.cannot_reblog": "This post cannot be boosted",
"status.comment": "Comment",
"status.copy": "Copy link to toot",
"status.delete": "Delete",
"status.detailed_status": "Detailed conversation view",
@ -435,6 +437,7 @@
"suggestions.header": "You might be interested in…",
"tabs_bar.federated_timeline": "Federated",
"tabs_bar.home": "Home",
"tabs_bar.tree": "Tree",
"tabs_bar.local_timeline": "Local",
"tabs_bar.notifications": "Notifications",
"tabs_bar.search": "Search",

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

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

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

@ -73,6 +73,7 @@
"column.notifications": "通知",
"column.pins": "置頂文章",
"column.public": "跨站時間軸",
"column.tree": "閉社樹",
"column_back_button.label": "返回",
"column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄左移",
@ -157,8 +158,8 @@
"empty_column.community": "本站時間軸暫時未有內容,快寫一點東西來搶頭香啊!",
"empty_column.direct": "你沒有個人訊息。當你發出或接收個人訊息,就會在這裡出現。",
"empty_column.domain_blocks": "尚未隱藏任何網域。",
"empty_column.favourited_statuses": "你還沒收藏任何文章。這裡將會顯示你收藏的嘟文。",
"empty_column.favourites": "還沒有人收藏這則文章。這裡將會顯示被收藏的嘟文。",
"empty_column.favourited_statuses": "你還沒喜歡任何嘟文。這裡將會顯示你喜歡的嘟文。",
"empty_column.favourites": "還沒有人喜歡這則嘟文。這裡將會顯示被喜歡的嘟文。",
"empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
"empty_column.hashtag": "這個標籤暫時未有內容。",
"empty_column.home": "你還沒有關注任何使用者。快看看{public},向其他使用者搭訕吧。",
@ -180,11 +181,11 @@
"generic.saved": "已儲存",
"getting_started.developers": "開發者",
"getting_started.directory": "個人資料目錄",
"getting_started.documentation": "文件",
"getting_started.documentation": "Mastodon文件",
"getting_started.heading": "開始使用",
"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": "服務條款",
"hashtag.column_header.tag_mode.all": "以及{additional}",
"hashtag.column_header.tag_mode.any": "或是{additional}",
@ -229,7 +230,7 @@
"keyboard_shortcuts.direct": "開啟私訊欄",
"keyboard_shortcuts.down": "在列表往下移動",
"keyboard_shortcuts.enter": "打開文章",
"keyboard_shortcuts.favourite": "收藏文章",
"keyboard_shortcuts.favourite": "喜歡",
"keyboard_shortcuts.favourites": "開啟最愛的內容",
"keyboard_shortcuts.federated": "打開跨站時間軸",
"keyboard_shortcuts.heading": "鍵盤快速鍵",
@ -379,7 +380,7 @@
"report.target": "舉報",
"search.placeholder": "搜尋",
"search_popout.search_format": "高級搜索格式",
"search_popout.tips.full_text": "輸入簡單的文字,搜索由你發放、收藏、轉推和提及你的文章,以及符合的使用者名稱,顯示名稱和標籤。",
"search_popout.tips.full_text": "輸入簡單的文字,搜索由你發放、喜歡、轉推和提及你的文章,以及符合的用戶名稱,帳號名稱和標籤。",
"search_popout.tips.hashtag": "標籤",
"search_popout.tips.status": "文章",
"search_popout.tips.text": "輸入簡單的文字,搜索符合的顯示名稱、使用者名稱和標籤",
@ -395,7 +396,8 @@
"status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉推",
"status.cannot_reblog": "這篇文章無法被轉推",
"status.copy": "將連結複製到文章中",
"status.comment": "評論",
"status.copy": "將連結複製到嘟文中",
"status.delete": "刪除",
"status.detailed_status": "詳細對話內容",
"status.direct": "私訊 @{name}",
@ -438,6 +440,7 @@
"tabs_bar.local_timeline": "本站",
"tabs_bar.notifications": "通知",
"tabs_bar.search": "搜尋",
"tabs_bar.tree": "閉社樹",
"time_remaining.days": "剩餘 {number, plural, one {# 天} other {# 天}}",
"time_remaining.hours": "剩餘 {number, plural, one {# 小時} other {# 小時}}",
"time_remaining.minutes": "剩餘 {number, plural, one {# 分鐘} other {# 分鐘}}",

+ 14
- 10
app/javascript/mastodon/locales/zh-TW.json View File

@ -64,8 +64,9 @@
"column.community": "本機時間軸",
"column.direct": "私訊",
"column.directory": "瀏覽個人資料",
"column.domain_blocks": "隱藏的網域",
"column.domain_blocks": "已封鎖的網域",
"column.favourites": "收藏",
"column.favourites": "喜歡",
"column.follow_requests": "關注請求",
"column.home": "首頁",
"column.lists": "名單",
@ -73,6 +74,7 @@
"column.notifications": "通知",
"column.pins": "釘選的嘟文",
"column.public": "聯邦時間軸",
"column.tree" : "閉社樹",
"column_back_button.label": "上一頁",
"column_header.hide_settings": "隱藏設定",
"column_header.moveLeft_settings": "將欄位向左移動",
@ -120,7 +122,7 @@
"confirmations.mute.explanation": "這將會隱藏來自他們的貼文與通知,但是他們還是可以查閱你的貼文與關注您。",
"confirmations.mute.message": "確定靜音 {name} ?",
"confirmations.redraft.confirm": "刪除並重新編輯",
"confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及收藏,且回覆這則的嘟文將會變成獨立的嘟文。",
"confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及喜歡,且回覆這則的嘟文將會變成獨立的嘟文。",
"confirmations.reply.confirm": "回覆",
"confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?",
"confirmations.unfollow.confirm": "取消關注",
@ -157,8 +159,8 @@
"empty_column.community": "本機時間軸是空的。快公開嘟些文搶頭香啊!",
"empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。",
"empty_column.domain_blocks": "尚未封鎖任何網域。",
"empty_column.favourited_statuses": "您還沒收藏過任何嘟文。當您收藏嘟文時,它將於此顯示。",
"empty_column.favourites": "還沒有人收藏過這則嘟文。當有人收藏嘟文時,它將於此顯示。",
"empty_column.favourited_statuses": "你還沒喜歡任何嘟文。這裡將會顯示你喜歡的嘟文。",
"empty_column.favourites": "還沒有人喜歡這則嘟文。這裡將會顯示被喜歡的嘟文。",
"empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
"empty_column.hashtag": "這個主題標籤下什麼也沒有。",
"empty_column.home": "您的首頁時間軸是空的!前往 {public} 或使用搜尋功能來認識其他人。",
@ -180,10 +182,10 @@
"generic.saved": "已儲存",
"getting_started.developers": "開發者",
"getting_started.directory": "個人資料目錄",
"getting_started.documentation": "文件",
"getting_started.documentation": "Mastodon文件",
"getting_started.heading": "開始使用",
"getting_started.invite": "邀請使用者",
"getting_started.open_source_notice": "Mastodon 是開源軟體。您可以在 GitHub {github} 上貢獻或是回報問題。",
"getting_started.open_source_notice": "閉社是開源軟體。你可以在 GitHub {github} 上貢獻或是回報問題。",
"getting_started.security": "帳號安全性設定",
"getting_started.terms": "服務條款",
"hashtag.column_header.tag_mode.all": "以及 {additional}",
@ -229,8 +231,8 @@
"keyboard_shortcuts.direct": "開啟私訊欄",
"keyboard_shortcuts.down": "在名單中往下移動",
"keyboard_shortcuts.enter": "檢視嘟文",
"keyboard_shortcuts.favourite": "加到收藏",
"keyboard_shortcuts.favourites": "開啟收藏名單",
"keyboard_shortcuts.favourite": "喜歡",
"keyboard_shortcuts.favourites": "開啟喜歡名單",
"keyboard_shortcuts.federated": "開啟聯邦時間軸",
"keyboard_shortcuts.heading": "鍵盤快速鍵",
"keyboard_shortcuts.home": "開啟首頁時間軸",
@ -289,7 +291,7 @@
"navigation_bar.discover": "探索",
"navigation_bar.domain_blocks": "隱藏的網域",
"navigation_bar.edit_profile": "編輯個人資料",
"navigation_bar.favourites": "收藏",
"navigation_bar.favourites": "喜歡",
"navigation_bar.filters": "靜音詞彙",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.follows_and_followers": "關注及關注者",
@ -394,7 +396,8 @@
"status.block": "封鎖 @{name}",
"status.bookmark": "書籤",
"status.cancel_reblog_private": "取消轉嘟",
"status.cannot_reblog": "這則嘟文無法被轉嘟",
"status.cannot_reblog": "這篇嘟文無法被轉嘟",
"status.comment": "評論",
"status.copy": "複製嘟文連結",
"status.delete": "刪除",
"status.detailed_status": "詳細的對話內容",
@ -438,6 +441,7 @@
"tabs_bar.local_timeline": "本機",
"tabs_bar.notifications": "通知",
"tabs_bar.search": "搜尋",
"tabs_bar.tree": "閉社樹",
"time_remaining.days": "剩餘{number, plural, one {# 天} other {# 天}}",
"time_remaining.hours": "剩餘{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/rtl';
@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;
$light-text-color: #444b5d;
$tc-background: $white !default;
//Newly added colors
$account-background-color: $white !default;

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

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

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

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

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

@ -4,7 +4,7 @@ $white: #ffffff; // White
$success-green: #79bd9a !default; // Padua
$error-red: #df405a !default; // Cerise
$warning-red: #ff5050 !default; // Sunset Orange
$gold-star: #ca8f04 !default; // Dark Goldenrod
$gold-star: #e04040 !default; // ::Change star to heart
$red-bookmark: $warning-red;
@ -43,6 +43,9 @@ $inverted-text-color: $ui-base-color !default;
$lighter-text-color: $ui-base-lighter-color !default;
$light-text-color: $ui-primary-color !default;
//timeline comeents
$tc-background: $classic-base-color !default;
// Language codes that uses CJK fonts
$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/footer.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/images/footer.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;

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

@ -80,6 +80,7 @@ class Formatter
end
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_custom_emojis(html, account.emojis, options[:autoplay]) if options[:custom_emojify]
html.html_safe # rubocop:disable Rails/OutputSafety
@ -109,8 +110,31 @@ class Formatter
html_entities.encode(html)
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 = {})
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)
options = accounts
@ -256,11 +280,13 @@ class Formatter
def link_to_url(entity, options = {})
url = Addressable::URI.parse(entity[:url])
html_attrs = { target: '_blank', rel: 'nofollow noopener noreferrer' }
html_attrs[:rel] = "me #{html_attrs[:rel]}" if options[:me]
html_attrs[:class] = "media-gallery__item-thumbnail" if entity[:img]
Twitter::Autolink.send(:link_to_text, entity, link_html(entity[:url]), url, html_attrs)
Twitter::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
encode(entity[:url])
end
@ -287,8 +313,15 @@ class Formatter
hashtag_html(entity[:hashtag])
end
def link_html(url)
def link_html(url, link_text, img)
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
text = url[prefix.length, 30]
suffix = url[prefix.length + 30..-1]
@ -302,6 +335,6 @@ class Formatter
end
def mention_html(account)
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{encode(account.username)}</span></a></span>"
"<span class=\"h-card\"><a href=\"#{encode(ActivityPub::TagManager.instance.url_for(account))}\" class=\"u-url mention\">@<span>#{format_display_name(account, custom_emojify: true)}</span></a></span>"
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)
case clause
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
{ match_phrase: { text: { query: clause.phrase } } }
else

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

@ -27,7 +27,7 @@
#
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
BLURHASH_OPTIONS = {

+ 5
- 2
app/models/status.rb View File

@ -86,8 +86,11 @@ class Status < ApplicationRecord
scope :remote, -> { where(local: false).where.not(uri: nil) }
scope :local, -> { where(local: true).or(where(uri: nil)) }
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_reblogs, -> { where('statuses.reblog_of_id IS NULL') }
#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, -> { joins('INNER JOIN statuses ori ON (statuses.reblog_of_id is NULL AND ori.id = statuses.id) OR ori.id = statuses.reblog_of_id')
.where('ori.account_id = statuses.account_id') }
scope :with_public_visibility, -> { where(visibility: :public) }
scope :tagged_with, ->(tag_ids) { joins(:statuses_tags).where(statuses_tags: { tag_id: tag_ids }) }
scope :in_chosen_languages, ->(account) { where(language: nil).or where(language: account.chosen_languages) }

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

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

+ 1
- 1
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 :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 :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create

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

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

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

@ -46,6 +46,7 @@ class FetchLinkCardService < BaseService
return @html if defined?(@html)
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'
@html = res.body_with_limit
@html_charset = res.charset

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

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

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

@ -1,7 +1,7 @@
# frozen_string_literal: true
class StatusLengthValidator < ActiveModel::Validator
MAX_CHARS = 500
MAX_CHARS = 5000
def validate(status)
return unless status.local? && !status.reblog?

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

@ -4,9 +4,11 @@
.fields-group
= 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_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
.block-icon= fa_icon 'warning'
= @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
%link{ rel: 'canonical', href: about_url }/
= render partial: 'shared/og'
@ -9,7 +6,7 @@
.landing__brand
= link_to root_url, class: 'brand' do
= svg_logo_full
%span.brand__tagline=t 'about.tagline'
%span.brand__tagline=site_title
.landing__grid
.landing__grid__column.landing__grid__column-registration
@ -38,7 +35,7 @@
%small= t('about.browse_public_posts')
.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
= fa_icon 'tablet fw'
= t('about.get_apps')

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

@ -70,13 +70,13 @@
.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')
- 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')
.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')
.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')
.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')
- unless whitelist_mode?
.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')
@ -84,13 +84,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')
.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')
.fields-group
= f.input :spam_check_enabled, as: :boolean, wrapper: :with_label, label: t('admin.settings.spam_check_enabled.title'), hint: t('admin.settings.spam_check_enabled.desc_html')
@ -107,7 +107,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'
.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 :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')

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

@ -14,10 +14,12 @@
= f.simple_fields_for :account do |ff|
.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
= 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
= 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
= 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'

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

@ -1,5 +1,5 @@
- 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
%meta{ name: 'robots', content: 'noindex' }/
@ -7,10 +7,10 @@
= 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')
- elsif @account.user_hides_network?
.nothing-here= t('accounts.network_hidden')
- elsif @follows.empty?
= nothing_here
- else

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

@ -1,5 +1,5 @@
- 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
%meta{ name: 'robots', content: 'noindex' }/
@ -7,10 +7,10 @@
= 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')
- elsif @account.user_hides_network?
.nothing-here= t('accounts.network_hidden')
- elsif @follows.empty?
= nothing_here
- else

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

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

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

@ -14,7 +14,7 @@
- unless whitelist_mode?
= 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.apps'), 'https://joinmastodon.org/apps', class: 'nav-link optional'
= link_to t('about.apps'), 'https://closed.social/apps', class: 'nav-link optional'
.nav-center
@ -52,6 +52,6 @@
%h4= t 'footer.more'
%ul
%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'

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

@ -58,9 +58,9 @@
·
= link_to remote_interaction_path(status, type: :reply), class: 'modal-button detailed-status__link' do
- if status.in_reply_to_id.nil?
= fa_icon('reply')
= fa_icon('comment')
- else
= fa_icon('reply-all')
= fa_icon('comments')
%span.detailed-status__reblogs>= number_to_human status.replies_count, strip_insignificant_zeros: true
= " "
·
@ -71,7 +71,7 @@
= " "
·
= 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>= number_to_human status.favourites_count, strip_insignificant_zeros: true
= " "

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

@ -57,9 +57,9 @@
.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
- if status.in_reply_to_id.nil?
= fa_icon 'reply fw'
= fa_icon 'comment fw'
- else
= fa_icon 'reply-all fw'
= fa_icon 'comments fw'
%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
- if status.distributable?
@ -69,4 +69,4 @@
- else
= fa_icon 'envelope fw'
= 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
= 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
- if @account.user&.setting_noindex

+ 1
- 1
config/application.rb View File

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

+ 2
- 2
config/environments/production.rb View File

@ -105,8 +105,8 @@ Rails.application.configure do
config.action_mailer.delivery_method = ENV.fetch('SMTP_DELIVERY_METHOD', 'smtp').to_sym
config.action_dispatch.default_headers = {
'Server' => 'Mastodon',
'X-Frame-Options' => 'DENY',
'Server' => 'ClosedSocial',
'X-Frame-Options' => 'SAMEORIGIN',
'X-Content-Type-Options' => 'nosniff',
'X-XSS-Protection' => '1; mode=block',
}

+ 1
- 1
config/initializers/content_security_policy.rb View File

@ -19,7 +19,7 @@ media_host ||= assets_host
Rails.application.config.content_security_policy do |p|
p.base_uri :none
p.default_src :none
p.frame_ancestors :none
p.frame_ancestors '*.closed.social', 'closed.social'
p.font_src :self, assets_host
p.img_src :self, :https, :data, :blob, assets_host
p.style_src :self, assets_host

+ 13
- 0
config/initializers/new_features.rb View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
Rails.application.configure do
config.x.email_default_domain = ENV.fetch('EMAIL_DEFAULT_DOMAIN') { '' }
config.x.email_regex = ENV.fetch('EMAIL_REGEX') { '.+' }
config.x.tree_address = ENV.fetch('TREE_ADDRESS') {''}
config.x.tree_acc = ENV.fetch('TREE_ACC') {'0'}
config.x.anon.tag = ENV.fetch('ANON_TAG') {'[mask]'}
config.x.anon.acc = ENV.fetch('ANON_ACC') {nil}
config.x.anon.namelist = ENV['ANON_NAME_LIST'] ? File.readlines(ENV['ANON_NAME_LIST']).collect(&:strip) : ['Alice', 'Bob', 'Carol', 'Dave']
config.x.anon.salt = (1..42).map { ('a'..'z').to_a.sample }.join
end

+ 8
- 8
config/locales/devise.zh-CN.yml View File

@ -23,28 +23,28 @@ zh-CN:
explanation: 你在 %{host} 上使用这个电子邮件地址创建了一个帐户。只需点击下面的链接,即可完成激活。如果你并没有创建过帐户,请忽略此邮件。
explanation_when_pending: 你用这个电子邮件申请了在 %{host} 注册。在确认电子邮件地址之后,我们会审核你的申请。在此之前,你不能登录。如果你的申请被驳回,你的数据会被移除,因此你无需再采取任何行动。如果申请人不是你,请忽略这封邮件。
extra_html: 请记得阅读<a href="%{terms_path}">本服务器的相关规定</a>和<a href="%{policy_path}">我们的使用条款</a>。
subject: Mastodon:来自 %{instance} 的确认指引
subject: 闭社:来自 %{instance} 的确认指引
title: 验证电子邮件地址
email_changed:
explanation: 你的帐户的电子邮件地址将变更为:
extra: 如果你并没有请求更改你的电子邮件地址,则他人很有可能已经入侵你的帐户。请立即更改你的密码;如果你已经无法访问你的帐户,请联系服务器管理员请求协助。
subject: Mastodon:电子邮件地址已被更改
subject: 闭社:电子邮件地址已被更改
title: 新电子邮件地址
password_change:
explanation: 你的帐户密码已更改。
extra: 如果你并没有请求更改你的密码,则他人很有可能已经入侵你的帐户。请立即更改你的密码;如果你已经无法访问你的帐户,请联系服务器的管理员请求协助。
subject: Mastodon:密码已被更改
subject: 闭社:密码已被更改
title: 密码已被重置
reconfirmation_instructions:
explanation: 点击下面的链接来确认你的新电子邮件地址。
extra: 如果你并没有请求本次变更,请忽略此邮件。Mastodon 帐户的电子邮件地址只有在你点击上面的链接后才会更改。
subject: Mastodon:确认 %{instance} 电子邮件地址
extra: 如果你并没有请求本次变更,请忽略此邮件。闭社帐户的电子邮件地址只有在你点击上面的链接后才会更改。
subject: 闭社:确认 %{instance} 电子邮件地址
title: 验证电子邮件地址
reset_password_instructions:
action: 更改密码
explanation: 点击下面的链接来更改帐户的密码。
extra: 如果你并没有请求本次变更,请忽略此邮件。你的密码只有在你点击上面的链接并输入新密码后才会更改。
subject: Mastodon:重置密码信息
subject: 闭社:重置密码信息
title: 重置密码
two_factor_disabled:
explanation: 帐号的双重认证已禁用。现在仅使用邮箱和密码登录即可登录。
@ -59,7 +59,7 @@ zh-CN:
subject: Mastodon:重新生成双重认证的恢复码
title: 双重验证的恢复码已更改
unlock_instructions:
subject: Mastodon:帐户解锁信息
subject: 闭社:帐户解锁信息
webauthn_credential:
added:
explanation: 以下安全密钥已添加到您的帐户
@ -83,7 +83,7 @@ zh-CN:
passwords:
no_token: 你必须通过密码重置邮件才能访问这个页面。如果你确实如此,请确保你输入的 URL 是完整的。
send_instructions: 几分钟后,你将收到重置密码的电子邮件。如果没有,请检查你的垃圾邮箱。
send_paranoid_instructions: 如果你的邮箱存在于我们的数据库中,你将收到一封找回密码的邮件。如果没有,请检查你的垃圾邮
send_paranoid_instructions: 如果你的邮箱存在于我们的数据库中,你将收到一封找回密码的邮件。如果没有,请检查你的垃圾邮
updated: 你的密码已修改成功,你现在已登录。
updated_not_active: 你的密码已修改成功。
registrations:

+ 19
- 18
config/locales/zh-CN.yml View File

@ -1,26 +1,26 @@
---
zh-CN:
about:
about_hashtag_html: 这里展示的是带有话题标签 <strong>#%{hashtag}</strong> 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 站点或与其兼容的网站上拥有一个帐户。
about_mastodon_html: Mastodon 是一个建立在开放式网络协议和自由、开源软件之上的社交网络,有着类似于电子邮件的分布式设计。
about_hashtag_html: 这里展示的是带有话题标签 <strong>#%{hashtag}</strong> 的公开嘟文。如果你想与他们互动,你需要在任意一个 Mastodon 站点或与其兼容的网站(例如闭社)上拥有一个帐户。
about_mastodon_html: 闭社 是一个基于Mastodon的建立在开放式网络协议和自由、开源软件之上的内容平台,有着类似于电子邮件的分布式设计。
about_this: 关于本站
active_count_after: 活跃
active_count_after: 近期上线
active_footnote: 每月活跃用户
administered_by: 本站管理员:
api: API
apps: 移动应用
apps_platforms: 在 iOS、Android 和其他平台上使用 Mastodon
browse_directory: 浏览用户目录并按兴趣筛选
apps_platforms: 在 iOS、Android 和其他平台上使用闭社
browse_directory: 浏览用户资料目录并按兴趣筛选
browse_local_posts: 浏览此服务器上实时公开嘟文
browse_public_posts: 浏览 Mastodon 上公共嘟文的实时信息流
browse_public_posts: 浏览闭社上公共嘟文的实时信息流
contact: 联系方式
contact_missing: 未设定
contact_unavailable: 未公开
discover_users: 发现用户
documentation: 文档
federation_hint_html: 在%{instance} 上拥有账号后,你可以关注任何 Mastodon 服务器或其他服务器上的人。
federation_hint_html: 在%{instance} 上拥有账户后,你可以关注其他闭社站点的人。
get_apps: 尝试移动应用
hosted_on: 一个在 %{domain} 上运行的 Mastodon 实例
hosted_on: 一个在 %{domain} 上运行的闭社实例
instance_actor_flash: '这个账号是个虚拟帐号,不代表任何用户,只用来代表服务器本身。它用于和其它服务器互通,所以不应该被封禁,除非你想封禁整个实例。但是想封禁整个实例的时候,你应该用域名封禁。
'
@ -32,7 +32,7 @@ zh-CN:
status_count_after:
other: 条嘟文
status_count_before: 他们共嘟出了
tagline: 关注并发现新朋友
tagline: 封闭,真是坏事吗?
terms: 使用条款
unavailable_content: 被限制的服务器
unavailable_content_description:
@ -48,7 +48,7 @@ zh-CN:
user_count_after:
other: 位用户
user_count_before: 这里共注册有
what_is_mastodon: Mastodon 是什么?
what_is_mastodon: 基于 Mastodon
accounts:
choices_html: "%{name} 的推荐:"
endorsements_hint: 您可以在web界面上推荐你关注的人,他们会出现在这里。
@ -1138,7 +1138,7 @@ zh-CN:
weibo: 新浪微博
current_session: 当前会话
description: "%{platform} 上的 %{browser}"
explanation: 你的 Mastodon 帐户目前已在这些浏览器上登录。
explanation: 你的闭社帐户目前已在这些浏览器上登录。
ip: IP 地址
platforms:
adobe_air: Adobe Air
@ -1162,7 +1162,7 @@ zh-CN:
aliases: 帐号别名
appearance: 外观
authorized_apps: 已授权的应用
back: 返回 Mastodon
back: 返回闭社
delete: 删除帐户
development: 开发
edit_profile: 更改个人资料
@ -1289,7 +1289,7 @@ zh-CN:
<p>您的公共内容可能会被网络中的其他服务器下载。 您的公开帖子和关注者帖子会发送到关注者所在的服务器,并且直接邮件会传递到收件人的服务器,只要这些关注者或收件人位于与此不同的服务器上。</p>
<p>当您授权应用程序使用您的帐户时,根据您批准的权限范围,它可能会访问您的公开个人资料信息,以下列表,您的关注者,您的列表,所有帖子和您的收藏夹。 应用程序永远不能访问您的电子邮件地址或密码。</p>
<p>当您授权应用程序使用您的帐户时,根据您批准的权限范围,它可能会访问您的公开个人资料信息,以下列表,您的关注者,您的列表,所有帖子和您的点赞记录。 应用程序永远不能访问您的电子邮件地址或密码。</p>
<hr class="spacer" />
@ -1315,6 +1315,7 @@ zh-CN:
contrast: Mastodon(高对比度)
default: Mastodon(暗色主题)
mastodon-light: Mastodon(亮色主题)
thu: closed.social(清华紫)
time:
formats:
default: "%Y年%m月%d日 %H:%M"
@ -1336,7 +1337,7 @@ zh-CN:
webauthn: 安全密钥
user_mailer:
backup_ready:
explanation: 你请求了一份 Mastodon 帐户的完整备份。现在你可以下载了!
explanation: 你请求了一份闭社帐户的完整备份。现在你可以下载了!
subject: 你的存档已经准备完毕
title: 存档导出
sign_in_token:
@ -1376,11 +1377,11 @@ zh-CN:
full_handle_hint: 你需要把这个告诉你的朋友们,这样他们就能从另一台服务器向你发送信息或者关注你。
review_preferences_action: 更改首选项
review_preferences_step: 记得调整你的偏好设置,比如你想接收什么类型的邮件,或者你想把你的嘟文可见范围默认设置为什么级别。如果你没有晕动病的话,考虑一下启用“自动播放 GIF 动画”这个选项吧。
subject: 欢迎来到 Mastodon
tip_federated_timeline: 跨站公共时间轴可以让你一窥更广阔的 Mastodon 网络。不过,由于它只显示你的邻居们所订阅的内容,所以并不是全部。
subject: 欢迎来到闭社
tip_federated_timeline: 跨站公共时间轴可以让你一窥更广阔的<b>闭社</b>网络。不过,由于它只显示你的邻居们所订阅的内容,所以并不是全部。
tip_following: 默认情况下,你会自动关注你所在服务器的管理员。想结交更多有趣的人的话,记得多逛逛本站时间轴和跨站公共时间轴哦。
tip_local_timeline: 本站时间轴可以让你一窥 %{instance} 上的用户。他们就是离你最近的邻居!
tip_mobile_webapp: 如果你的移动设备浏览器允许你将 Mastodon 添加到主屏幕,你就能够接收推送消息。它就像本地应用一样好使!
tip_mobile_webapp: 如果你的移动设备浏览器允许你将<b>闭社</b>添加到主屏幕,你就能够接收推送消息。它就像本地应用一样好使!
tips: 小贴士
title: "%{name},欢迎你的加入!"
users:
@ -1396,7 +1397,7 @@ zh-CN:
signed_in_as: 当前登录的帐户:
suspicious_sign_in_confirmation: 你似乎没有在这台设备上登录过,并且你也有很久没有登录过了,所以我们给你的电子邮箱发了封邮件,想确认一下确实是你。
verification:
explanation_html: 您可以 <strong>验证自己是个人资料元数据中的某个链接的所有者</strong>。 为此,被链接网站必须包含一个到您的 Mastodon 主页的链接。链接中 <strong>必须</strong> 包括 <code>rel="me"</code> 属性。链接的文本内容可以随意填写。例如:
explanation_html: 您可以 <strong>验证自己是个人资料元数据中的某个链接的所有者</strong>。 为此,被链接网站必须包含一个到您的闭社主页的链接。链接中 <strong>必须</strong> 包括 <code>rel="me"</code> 属性。链接的文本内容可以随意填写。例如:
verification: 验证
webauthn_credentials:
add: 添加新的安全密钥

+ 4
- 0
config/routes.rb View File

@ -521,6 +521,10 @@ Rails.application.routes.draw do
get '/about/more', to: 'about#more'
get '/terms', to: 'about#terms'
get '/jump/:destin/(*path)', to: 'about#jump', :constraints => { :destin => /[0-9a-zA-Z\._-]+/ }, :format => 'html'
get '/my_data/:year', to: 'about#my_data'
match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false
match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false
end

+ 1
- 0
config/themes.yml View File

@ -1,3 +1,4 @@
default: styles/application.scss
contrast: styles/contrast.scss
mastodon-light: styles/mastodon-light.scss
thu: styles/thu.scss

+ 28
- 4
db/schema.rb View File

@ -189,8 +189,8 @@ ActiveRecord::Schema.define(version: 2020_12_18_054746) do
t.integer "avatar_storage_schema_version"
t.integer "header_storage_schema_version"
t.string "devices_url"
t.integer "suspension_origin"
t.datetime "sensitized_at"
t.integer "suspension_origin"
t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin
t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true
t.index ["moved_to_account_id"], name: "index_accounts_on_moved_to_account_id"
@ -467,12 +467,12 @@ ActiveRecord::Schema.define(version: 2020_12_18_054746) do
end
create_table "ip_blocks", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "expires_at"
t.inet "ip", default: "0.0.0.0", null: false
t.integer "severity", default: 0, null: false
t.datetime "expires_at"
t.text "comment", default: "", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "list_accounts", force: :cascade do |t|
@ -821,6 +821,30 @@ ActiveRecord::Schema.define(version: 2020_12_18_054746) do
t.index ["tag_id", "status_id"], name: "index_statuses_tags_on_tag_id_and_status_id", unique: true
end
create_table "stream_entries", force: :cascade do |t|
t.bigint "activity_id"
t.string "activity_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "hidden", default: false, null: false
t.bigint "account_id"
t.index ["account_id", "activity_type", "id"], name: "index_stream_entries_on_account_id_and_activity_type_and_id"
t.index ["activity_id", "activity_type"], name: "index_stream_entries_on_activity_id_and_activity_type"
end
create_table "subscriptions", force: :cascade do |t|
t.string "callback_url", default: "", null: false
t.string "secret"
t.datetime "expires_at"
t.boolean "confirmed", default: false, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "last_successful_delivery_at"
t.string "domain"
t.bigint "account_id", null: false
t.index ["account_id", "callback_url"], name: "index_subscriptions_on_account_id_and_callback_url", unique: true
end
create_table "system_keys", force: :cascade do |t|
t.binary "key"
t.datetime "created_at", null: false

+ 2
- 2
lib/mastodon/version.rb View File

@ -21,7 +21,7 @@ module Mastodon
end
def suffix
''
'+闭社'
end
def to_a
@ -33,7 +33,7 @@ module Mastodon
end
def repository
ENV.fetch('GITHUB_REPOSITORY', 'tootsuite/mastodon')
ENV.fetch('GITHUB_REPOSITORY', 'closed-social/mastodon')
end
def source_base_url

+ 2
- 0
package.json View File

@ -130,6 +130,7 @@
"react-hotkeys": "^1.1.4",
"react-immutable-proptypes": "^2.2.0",
"react-immutable-pure-component": "^2.2.2",
"react-html-parser": "^2.0.2",
"react-intl": "^2.9.0",
"react-masonry-infinite": "^1.2.2",
"react-motion": "^0.5.2",
@ -144,6 +145,7 @@
"react-swipeable-views": "^0.13.9",
"react-textarea-autosize": "^8.3.0",
"react-toggle": "^4.1.1",
"react-tree-graph": "^4.0.1",
"redis": "^3.0.2",
"redux": "^4.0.5",
"redux-immutable": "^4.0.0",

BIN
View File


BIN
View File


+ 14
- 0
public/auto_comp_email.js View File

@ -0,0 +1,14 @@
if(navigator.userAgent.search('MicroMessenger') !== -1)
location.href = `https://closed.social/tools/safe_jump/?go=${encodeURIComponent(location.href)}&t=${encodeURIComponent(document.title)}`;
var em = document.getElementById("registration_user_email");
if(!em)
em = document.getElementById("user_email");
var ap = em.nextSibling;
em.addEventListener("blur", function( event ) {
if(ap.style.display != 'none' && em.value) {
em.value+=ap.innerText;
ap.style.display = 'none';
//alert('注意:清华邮箱收取外部邮件会有至多十分钟的延迟,完成注册后请稍后再查收邮件。请务必确保邮箱正确,闭社已经遇到了大量无效邮箱(例如漏掉了数字)')
}
});

BIN
View File


BIN
View File


BIN
View File


+ 1
- 1
public/mask-icon.svg View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 94.023018 100.80365" height="28.44903mm" width="26.535385mm"><path d="M72.57077 49.00625c-3.9125 0-7.085-3.1825-7.085-7.095 0-3.91125 3.1725-7.09375 7.085-7.09375 3.92125 0 7.09375 3.1825 7.09375 7.09375 0 3.9125-3.1725 7.095-7.09375 7.095m-25.55875 0c-3.9225 0-7.095-3.1825-7.095-7.095 0-3.91125 3.1725-7.09375 7.095-7.09375 3.91125 0 7.09375 3.1825 7.09375 7.09375 0 3.9125-3.1825 7.095-7.09375 7.095m-25.57 0c-3.91125 0-7.08375-3.1825-7.08375-7.095 0-3.91125 3.1725-7.09375 7.08375-7.09375 3.92125 0 7.09375 3.1825 7.09375 7.09375 0 3.9125-3.1725 7.095-7.09375 7.095m72.5775-15.905c0-21.86625-14.32375-28.27375-14.32375-28.27375-7.23-3.31875-19.63-4.7125-32.5175-4.8275h-.3125c-12.88875.115-25.28875 1.50875-32.5075 4.8275 0 0-14.32375 6.4075-14.32375 28.27375 0 5.00375-.105 10.995.05125 17.34C.60577 71.83 4.00702 92.905 23.78327 98.1375c9.1125 2.4125 16.945 2.9125 23.24875 2.56875 11.4225-.63375 17.84-4.07625 17.84-4.07625l-.37375-8.3025s-8.16625 2.58-17.34125 2.2675c-9.09125-.3125-18.6825-.9775-20.16-12.13875-.135-.97875-.1975-2.02875-.1975-3.13125 0 0 8.915 2.185 20.2325 2.69375 6.9075.3225 13.39875-.39375 19.98375-1.185 12.6275-1.50875 23.62375-9.29 25.0075-16.405 2.17375-11.1925 1.99625-27.3275 1.99625-27.3275" fill="#000"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="15 25 2 30" height="28.44903mm" width="26.535385mm"><path d="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"/></svg>

BIN
View File


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

Loading…
Cancel
Save