Compare commits

...

Author SHA1 Message Date
  欧醚 20e797a51d fix permission 1 year ago
  欧醚 9a1e8d0f4b Merge tag 'v3.4.6' into cs+3.4.6 2 years ago
  欧醚 4390b676da 加强邮箱保护 2 years ago
  欧醚 a9554a1c9b 展示转嘟,替代 1d0ddd33b6 的临时措施 2 years ago
  Claire 93a6c143af
Fix insufficient sanitization of report comments (#17430) 2 years ago
  Claire bb7b2868a0 Bump version to 3.4.6 2 years ago
  Wonderfall a06dda41d0 disable legacy XSS filtering (#17289) 2 years ago
  Claire bf005edd30 Change mastodon:webpush:generate_vapid_key task to not require functional env (#17338) 2 years ago
  Claire df68d2eab8 Fix response_to_recipient? CTE 2 years ago
  Claire b27f50da5a Fix insufficient sanitization of report comments 2 years ago
  Claire e2009ced3a Fix compacted JSON-LD possibly causing compatibility issues on forwarding 2 years ago
  Puck Meerburg fe0210074f Compact JSON-LD signed incoming activities 2 years ago
  Claire c8dbbd60eb Fix error-prone SQL queries (#15828) 2 years ago
  Claire 6d831fe274
Fix spurious errors when receiving an Add activity for a private post (#17425) 2 years ago
  Claire 1c8c318281 Bump version to 3.4.5 2 years ago
  Claire d722222fe1 Add more advanced migration tests (#17393) 2 years ago
  Claire 03f0e98b32 Fix followers synchronization mechanism not working when URI has empty path (#16510) 2 years ago
  Eugen Rochko 2c83b9076d Add manual GitHub Actions runs (#17000) 2 years ago
  Eugen Rochko c8301bcfc3 Change workflow to push to Docker Hub (#16980) 2 years ago
  Yusuke Nakamura 0ae91e45de Build container image by GitHub Actions (#16973) 2 years ago
  Claire 2363b026e6 Bump ruby-saml from 1.11.0 to 1.13.0 (#16723) 2 years ago
  Jeong Arm 959234c1e4 Save bundle config as local (#17188) 2 years ago
  Claire 0dc103ea11 Fix edge case in migration helpers that caused crash because of PostgreSQL quirks (#17398) 2 years ago
  Claire b782f86b51 Fix some old migration scripts (#17394) 2 years ago
  Eugen Rochko fd868f8ca0 Bump version to 3.4.4 2 years ago
  Claire 4cd33a2c71 Fix "bundle exec rails mastodon:setup" crashing in some circumstances (#16976) 2 years ago
  Claire f264cca1d2 Fix filtering DMs from non-followed users (#17042) 2 years ago
  Claire 5e4b04de88 Fix handling of recursive toots in WebUI (#17041) 2 years ago
  Claire 3c18311d86 Fix error when suspending user with an already-existing canonical email block (#17036) 2 years ago
  Claire e5113a8cad Fix overflow of long profile fields in admin view (#17010) 2 years ago
  Claire 22cd1e6ab5 Fix confusing error when webfinger request returns empty document (#16986) 2 years ago
  Claire e65ede1ac5 Fix upload of remote media with OpenStack Swift sometimes failing (#16998) 2 years ago
  Takeshi Umeda 1bcb3daf7e Fix logout link not working in safari (#16574) 2 years ago
  Claire 9c610ca0a4 Fix “open” link of media modal not closing modal (#16524) 2 years ago
  Claire 77d0297313 Fix replying from modal (#16516) 2 years ago
  Eugen Rochko 4b6668868e Bump version to 3.4.3 2 years ago
  Eugen Rochko 5c47a18c8d Fix login being broken due to inaccurately applied backport fix in 3.4.2 2 years ago
  Eugen Rochko 8a74d851d2 Bump version to 3.4.2 2 years ago
  Claire 76c2028859 Fix AccountNote not having a maximum length (#16942) 2 years ago
  Claire 3251b8eead Fix reviving revoked sessions and invalidating login (#16943) 2 years ago
  Claire f60bb0784f Fix handling announcements with links (#16941) 2 years ago
  Claire c3a6f7b941 Fix user email address being banned on self-deletion (#16503) 2 years ago
  Claire 986397b3a2 Improve modal flow and back button handling (#16499) 2 years ago
  Claire c79d4711e9 Change references to tootsuite/mastodon to mastodon/mastodon (#16491) 2 years ago
  Claire be56033715 Change number_to_human calls to always use 3-digits precision (#16469) 2 years ago
  Claire 8815e98aa2 Fix pop-in player display when poster has long username or handle (#16468) 2 years ago
  Claire 4bc1fde105 Fix anonymous access to outbox not being cached by the reverse proxy (#16458) 2 years ago
  Claire 34ab4111a7 Fix WebUI crash when a toot with a playing video gets deleted (#16384) 2 years ago
  Claire aebcb722aa Fix serialization of followers/following counts when user hides their network (#16418) 2 years ago
  Claire 9a468c895b Fix inefficiencies in auto-linking code (#16506) 2 years ago
  Claire a1e5ff04e3 Fix tootctl self-destruct not sending Delete activities for recently-suspended accounts (#16688) 2 years ago
  Claire e40d5414cc Fix crashes with Microsoft Translate on Microsoft Edge (#16525) 2 years ago
  Claire 40eaa8706b Fix suspicious sign-in mail text being out of date (#16690) 2 years ago
  Claire 4cc7efcb08 Fix some Rails frameworks being unnecessarily loaded (#16725) 2 years ago
  Claire 9b34647c9b Fix followers synchronization mechanism not working when URI has empty path (#16744) 2 years ago
  Eugen Rochko 6b98fd0b4f Fix not being able to suspend accounts that already have a canonical e-mail block (#16455) 2 years ago
  Claire c7f534ab95 Fix missing on_delete: :cascade for canonical_email_blocks foreign key (#16448) 2 years ago
  Eugen Rochko d5a50e9dfb Add `configuration` attribute to `GET /api/v1/instance` (#16485) 2 years ago
  Jeong Arm e1cf8d4d37 Fix statuses order in account's statuses admin page (#16937) 2 years ago
  Jeong Arm f366a23a23 Skip blocked domains media on tootctl media refresh (#16914) 2 years ago
  Claire aa828aea02 Fix mastodon:setup to take dotenv/docker-compose differences into account (#16896) 2 years ago
  Claire 123a88b6b5 Fix some link previews being incorrectly generated from other prior links (#16885) 2 years ago
  Claire e63370db19 Fix scheduled statuses decreasing statuses counts (#16791) 2 years ago
  Claire 2396c9061a Fix webauthn secure key authentication (#16792) 2 years ago
  Holger 663b58aaae use relative path for `scope` (#16714) 2 years ago
  Claire 75441ac63d Fix addressing of remote groups' followers (#16700) 2 years ago
  Claire 5899fe70b6 Fix processing mentions to domains with non-ascii TLDs (#16689) 2 years ago
  Claire 2688f18d06 Fix authentication failures after going halfway through a sign-in attempt (#16607) 2 years ago
  Claire f51c6cba1f Fix remotely-suspended accounts' toots being merged back into timelines (#16628) 2 years ago
  Claire 4f852448e1 Fix crash when encountering invalid account fields (#16598) 2 years ago
  Takeshi Umeda c02d6c46e3 Fix invalid blurhash handling in Create activity (#16583) 2 years ago
  Takeshi Umeda 987f945930 Fix when MoveWorker cannot get locale from remote account (#16576) 2 years ago
  Claire e62f488be5 Fix newlines in accout notes added by the Move handler (#16415) 2 years ago
  TA 0b7b25d17c remove unrendered HTML b tag from welcome email 2 years ago
  欧醚 eede6ca0c5 change version suffix to fix push notification 2 years ago
  欧醚 fa687d53a6 show toot chars limit and poll limit in instance api (for mobile app) 2 years ago
  欧醚 910de600d5 修改投票选项数上限 2 years ago
  Claire 2476d4f391 Fix user email address being banned on self-deletion (#16503) 2 years ago
  欧醚 34927adb74 兼容不开启闭社树 2 years ago
  欧醚 4db2290525 Merge tag 'v3.4.1' into closed-social-v3 2 years ago
  TA 5dd4f64b9f [fix] background image of THU 2 years ago
  欧醚 1d0ddd33b6 临时措施 2 years ago
  TA 3312ec5e9b strip for anonymous status 3 years ago
  TA 50101ad5f2 temporarily disable security code email 3 years ago
  TA 14630f3f48 fix hour format temporarily 3 years ago
  欧醚 fbf63eef29 Merge v3.3.0 into closed-social-v3 3 years ago
  欧醚 09c1e377a7 fix detail 3 years ago
  欧醚 d3625fa0f4 feat: 年度数据 fix some detail & 允许管理员查询其他人 3 years ago
  欧醚 fd8dd1498d feat: 年度数据统计 3 years ago
  欧醚 f0c6d72e60 配置参数说明 3 years ago
  欧醚 84d2e402d3 匿名每天刷新 3 years ago
  欧醚 a583987d76 Merge pull request 'fix bugs' (#5) from use-display-name-for-mention into closed-social-v3 3 years ago
  欧醚 c148e7a041 fix bugs 3 years ago
  欧醚 2b34c72fc4 Merge pull request 'use display_name for mention' (#4) from use-display-name-for-mention into closed-social-v3 3 years ago
  欧醚 ff0da1940b use display_name for mention 3 years ago
  欧醚 0d55e30e9e 修复界面bug 3 years ago
  欧醚 54cf68d8c6 增加前端邮箱正则限制 3 years ago
  欧醚 48cf6d253e make api/v1/instance public 3 years ago
  欧醚 04bfe40736 Merge remote-tracking branch 'origin/master' into closed-social-v3 3 years ago
  Junxi Song 3140a72fd7 Fixed the problem of fixed comments. 3 years ago
  欧醚 2cbee8feda 删除空格 3 years ago
  欧醚 53e44cc118 删除关注tag功能 3 years ago
  欧醚 eee3899b2c Merge remote-tracking branch 'origin/master' into closed-social-v3 3 years ago
  Junxi Song 4b80abced1 更新英文,台湾,香港表述# 3 years ago
  欧醚 bdb623f9eb 版本号后缀及默认仓库 3 years ago
  欧醚 98fe3af7b2 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 38632675b1 fix bugs 3 years ago
  欧醚 aa139841e5 恢复界面 3 years ago
  欧醚 b33a2295a9 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 f4e504e1fb 修复注册页面的显示 3 years ago
  欧醚 445ff0ed5f Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 3b950848ca Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 b6e7399e9e Merge tag 'v3.2.0' into closed-social-v3 3 years ago
  欧醚 7a9863cef7 中文搜索使用ik分词器 3 years ago
  欧醚 2a780b1118 og:image 允许不规范的 image/jpg (兼容网易云音乐) 3 years ago
  欧醚 c96e2c1d8f 跨站栏显示热门 3 years ago
  欧醚 57f5128fc6 fix bug (static variables are not thread safe) 3 years ago
  欧醚 d67cd9a573 微调界面 3 years ago
  欧醚 1b666873a2 允许whitelist_mode显示更多内容 3 years ago
  欧醚 c4897a3157 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 89bc52e250 微调about界面 3 years ago
  欧醚 bd9607f5c6 preview card 兼容有重复content-type头的情况 3 years ago
  欧醚 478efa8bf9 fix bug 3 years ago
  欧醚 20e0c1f803 重新实现了获取context 3 years ago
  欧醚 8ee92eab43 优化UI 3 years ago
  欧醚 a93e09ed9d 缩短加载评论延时,对所有嘟文加载评论 3 years ago
  欧醚 d39498f027 删除了所有关于清华邮箱延迟的提醒 3 years ago
  欧醚 0646238fed fix bug of mastodon(?): add :ssl in smtp setting 3 years ago
  欧醚 3dfac8387d fix bugs && 匿名可禁言 3 years ago
  欧醚 747ac90294 fix bug && 取消不必要的修改 3 years ago
  欧醚 57c905a689 minor change 3 years ago
  欧醚 6e1eb19ae0 删除硬编码ws地址 3 years ago
  欧醚 192657c54b 标记匿名标签刷新 超过一天未使用自动刷新 3 years ago
  欧醚 32cb3aa8d9 闭社树树根帐号可配置 对所有嘟文提供树形结构 3 years ago
  欧醚 720f8fc9d1 fix bug 3 years ago
  欧醚 95ff45bb7d 更好的匿名 3 years ago
  欧醚 cdf0e96549 优化了markdown外链图片的显示格式 3 years ago
  欧醚 24d6a043e5 删除markdown库,修改linkify实现嘟文/公告支持markdown语法的链接,彻底清理置顶栏 3 years ago
  欧醚 3149fbd706 删除置顶栏目,公告使用markdown 3 years ago
  欧醚 c247330f05 Merge branch 'master' of https://github.com/tootsuite/mastodon into closed-social-v3 3 years ago
  欧醚 3cc3992d1d 删除orig 修改细节 匿名可配置 3 years ago
  欧醚 9312eab8de fix bug && 小改动 3 years ago
  欧醚 d5cc1beb73 Merge branch 'master' into closed-social-v3 3 years ago
  欧醚 7f54b403de 细节 4 years ago
  欧醚 4eb82a685f fix bugs 4 years ago
  欧醚 2a094835e9 清华紫主题恢复显示吉祥物 4 years ago
  欧醚 0b5b741e32 jump 处理参数 4 years ago
  欧醚 a0c0005808 fix bugs 4 years ago
  欧醚 bc3df81610 修改jump跳转 4 years ago
  欧醚 09688a0535 Merge branch 'closed-social-v3' of github.com:closed-social/mastodon into closed-social-v3 4 years ago
  欧醚 d7400b4d34 /jump 登陆后跳转功能 4 years ago
  欧醚 ca716e07ef mask不同天使用不同的编码规则 4 years ago
  欧醚 b323bc8e15 默认白名单模式,修改apple icon 4 years ago
  欧醚 97cfee7886 白名单模式下仍然提供about about/more 页面 4 years ago
  欧醚 b39c079cec 匿名使用汉字编号 4 years ago
  欧醚 7848876404 每日刷新匿名编号 4 years ago
  欧醚 459942e2ea 允许任何时候匿名 4 years ago
  欧醚 bb9c5f822e 匿名评论有简单数字代号 4 years ago
  欧醚 ac744cd462 取消错误的修改(直接使用mastodon的白名单模式控制权限),针对微信内置浏览器跳转 4 years ago
  欧醚 dbd802d13b 修改闭社树上内容转嘟的引用显示 4 years ago
  欧醚 29c865dbc8 fix bugs 4 years ago
  欧醚 aeb8046cb5 fix bugs;闭社树按时间倒序 4 years ago
  欧醚 c608897a29 Merge branch 'closed-social-v3' of github.com:closed-social/mastodon into closed-social-v3 4 years ago
  欧醚 4f1f2540a4 评论预览显示两层,微调显示界面 4 years ago
  欧醚 3197fd21e4 修复修改邮箱时不检查白名单的问题 4 years ago
  欧醚 40533e194f 调整了左右滑动的阈值 4 years ago
  欧醚 0efe1c54e9 为ws单独指定域名(解决CDN不支持ws的问题),放宽api频率 4 years ago
  欧醚 d00d6415fe Fix reuse of detailed status components 4 years ago
  欧醚 4103b2a466 重新整理了评论/回复图标及文字 4 years ago
  欧醚 e701ceb033 删除了旧的点赞动画 4 years ago
  欧醚 2bb0aaa47c Merge branch 'closed-social-v3' of https://github.com/closed-social/mastodon into closed-social-v3 4 years ago
  欧醚 e389037482 修改了喜欢的图标颜色和动画 4 years ago
  欧醚 33e1c8680e 闭社树根节点只加载一层 4 years ago
  欧醚 6bd0848a0b 修改frame规则 4 years ago
  欧醚 dd16a8f4ac 描述为https链接的的图片显示为iframe 4 years ago
  欧醚 604cffbeed fix bugs 4 years ago
  欧醚 112a3f0754 时间轴只显示对自己的转嘟 4 years ago
  欧醚 0414dccfe9 闭社树每次只加载两层 4 years ago
  欧醚 ec25fe716e 不对不可转嘟问显示转嘟数,private嘟文强制请求context 4 years ago
  欧醚 50439d7218 延时自动加载评论,不再需要鼠标经过/点击 4 years ago
  欧醚 f8a2a10284 显示引用以实现转评 4 years ago
  欧醚 78bacf9626 取消热门tag门槛,清华邮箱的额外提醒信息 4 years ago
  欧醚 49a2a2ef96 修复bug 4 years ago
  欧醚 dc6403acac 为高级web模式设置置顶栏 4 years ago
  欧醚 0dbbca12c0 修改文字,解决previewcard被反爬虫的问题,增长Timeout 4 years ago
  欧醚 56f55defc2 修改了移动应用、指南(文档)的链接,修改了消息图标 4 years ago
  欧醚 7a02ce51be 修改了评论预览的结构和显示方式(增加了wrapper) 4 years ago
  欧醚 774235c1f5 修改了评论预览的展开方式 4 years ago
  欧醚 08e12eadaf 修改默认头像,修改style 4 years ago
  欧醚 36ec3f043c 修改style,置顶栏可关闭 4 years ago
  欧醚 b9541d6adc 修复style 4 years ago
  欧醚 be62d2550f 修复背景图显示 4 years ago
  欧醚 c4ff322c3a 新增主题,并迁移部分自定义css到主题 4 years ago
  欧醚 1c90891868 置顶栏支持富文本 4 years ago
  欧醚 d2d99f86dd 仅[pub]开头的嘟文站外可见 4 years ago
  欧醚 e5c6a02ae4 修复favicon, 为所有评论显示show thread 4 years ago
  欧醚 f9fe84914a 不在时间线显示自回复 4 years ago
  欧醚 c67bf258ab 修改图标 4 years ago
  欧醚 c2dc7566c1 置顶消息,回复API限制,修改闭社树地址配置方式 4 years ago
  欧醚 9b76f5bb43 优化了闭社树的显示 4 years ago
  欧醚 eae877d063 修改了评论预览的处理逻辑,移动端第一次点击加载评论预览,第二次点击打开嘟文 4 years ago
  欧醚 1db7d22058 修改后端 赞藏 -> 喜欢 4 years ago
  欧醚 3b326e3a55 字数限制 500 -> 5000 4 years ago
  欧醚 24859251dc 本站栏显示热门tag榜 4 years ago
  欧醚 6a4bfbf621 禁止非closed.social的mastodon实例 Warning:硬编码 4 years ago
  欧醚 a31aaeef04 change email info and force to use zh-CN 4 years ago
  欧醚 8fa6fcf097 仅在鼠标移过/手机按下时加载评论预览 4 years ago
  欧醚 062308c006 放宽了非认证api频率限制 4 years ago
  欧醚 a83a3e78fc 放宽了api频率限制 4 years ago
  欧醚 a04935f5c1 树洞评论匿名 4 years ago
  欧醚 66561200b5 show comments on timeline, and show right count when unfavourite/unreblog 4 years ago
  欧醚 3142db432d set default email domain in ENV 4 years ago
  欧醚 5b9cc9c93b change 赞藏 to 喜欢, and change logo_alt 4 years ago
  欧醚 718814bfbf give up pinned statuses, and add tree to navigation/tabs_bar, with funny way 4 years ago
  欧醚 713bf5509f add 闭社树 to navigation, with horrible way 4 years ago
  欧醚 40ea60e578 show reblogs on timeline 4 years ago
  欧醚 6dd757a495 change x and y of text on tree 4 years ago
  欧醚 4f2275c1e5 click the node on tree to go to the status 4 years ago
  欧醚 0a8bcfade5 fix bug for tree animate. Use statusId as keyPro 4 years ago
  欧醚 fb82f7b6ac now you can click nodes on 闭社树 4 years ago
  欧醚 2bb6238dc8 change size of tree graph 4 years ago
  欧醚 a8e4c3f5a2 now can draw 闭社树 4 years ago
  欧醚 6be4def2ec follow tags 4 years ago
  欧醚 0101dfcdfd fix bug 4 years ago
  欧醚 a5db5d45a2 fix bug 4 years ago
  欧醚 b0aadae668 show tree in className 4 years ago
  欧醚 f0ad5eaea7 pin statuses pinned by Account(1) 4 years ago
  欧醚 353b88eba0 require login for tag api 4 years ago
  欧醚 3fd2acbacb show depth and only show direct sons for 闭社树 4 years ago
  欧醚 457daf4353 better hide to unsigned user 4 years ago
  欧醚 60a876f39a change 收藏 to 赞藏, and hide wrong active_user_count 4 years ago
  欧醚 489c93f011 show comments on timeline, with ugly way 4 years ago
  欧醚 3c25a96aad show comments on timeline, with bugs 4 years ago
  欧醚 990d264f63 show fav/reb number 4 years ago
  欧醚 5eb86661ba set max trends number to 5 4 years ago
  欧醚 207b875ded hide account info to unsigned user 4 years ago
  欧醚 3f178bbdae change all icon of fav/rep 4 years ago
  欧醚 66919577e8 change some icon of fav/rep 4 years ago
  欧醚 c2c5c22ee6 fix bugs 4 years ago
  欧醚 79f21b2d8f fix favicon.ico 4 years ago
  欧醚 1abf9c6d92 merge changes 4 years ago
251 changed files with 5108 additions and 2464 deletions
Split View
  1. +41
    -1
      .circleci/config.yml
  2. +6
    -0
      .env.production.sample
  3. +1
    -1
      .github/CODEOWNERS
  4. +34
    -0
      .github/workflows/build-image.yml
  5. +6
    -0
      .gitignore
  6. +2
    -2
      AUTHORS.md
  7. +1414
    -1325
      CHANGELOG.md
  8. +2
    -2
      CONTRIBUTING.md
  9. +30
    -0
      CS_CONF.md
  10. +2
    -2
      Dockerfile
  11. +3
    -2
      Gemfile.lock
  12. +4
    -4
      README.md
  13. +2
    -2
      app.json
  14. +1
    -0
      app/chewy/statuses_index.rb
  15. +35
    -1
      app/controllers/about_controller.rb
  16. +2
    -2
      app/controllers/activitypub/followers_synchronizations_controller.rb
  17. +9
    -1
      app/controllers/activitypub/outboxes_controller.rb
  18. +1
    -1
      app/controllers/admin/statuses_controller.rb
  19. +2
    -2
      app/controllers/api/v1/instances/activity_controller.rb
  20. +2
    -2
      app/controllers/api/v1/instances/peers_controller.rb
  21. +1
    -1
      app/controllers/api/v1/instances_controller.rb
  22. +17
    -3
      app/controllers/api/v1/statuses_controller.rb
  23. +2
    -1
      app/controllers/api/v1/timelines/public_controller.rb
  24. +0
    -1
      app/controllers/auth/passwords_controller.rb
  25. +0
    -3
      app/controllers/auth/registrations_controller.rb
  26. +11
    -10
      app/controllers/auth/sessions_controller.rb
  27. +12
    -9
      app/controllers/concerns/sign_in_token_authentication_concern.rb
  28. +13
    -11
      app/controllers/concerns/two_factor_authentication_concern.rb
  29. +1
    -1
      app/controllers/follower_accounts_controller.rb
  30. +1
    -1
      app/controllers/following_accounts_controller.rb
  31. +1
    -1
      app/controllers/home_controller.rb
  32. +1
    -1
      app/controllers/settings/deletes_controller.rb
  33. +2
    -1
      app/controllers/well_known/webfinger_controller.rb
  34. +3
    -3
      app/helpers/accounts_helper.rb
  35. +18
    -1
      app/helpers/application_helper.rb
  36. +55
    -0
      app/helpers/context_helper.rb
  37. +10
    -2
      app/helpers/home_helper.rb
  38. +80
    -0
      app/helpers/jsonld_helper.rb
  39. +1
    -1
      app/javascript/images/logo.svg
  40. +1
    -1
      app/javascript/images/logo_alt.svg
  41. +5
    -1
      app/javascript/images/logo_full.svg
  42. +1
    -1
      app/javascript/images/logo_transparent.svg
  43. +1
    -1
      app/javascript/images/logo_transparent_black.svg
  44. BIN
     
  45. BIN
     
  46. +8
    -2
      app/javascript/mastodon/actions/interactions.js
  47. +14
    -7
      app/javascript/mastodon/actions/picture_in_picture.js
  48. +4
    -1
      app/javascript/mastodon/actions/timelines.js
  49. +1
    -1
      app/javascript/mastodon/components/intersection_observer_article.js
  50. +12
    -1
      app/javascript/mastodon/components/media_gallery.js
  51. +40
    -0
      app/javascript/mastodon/components/modal_root.js
  52. +3
    -4
      app/javascript/mastodon/components/scrollable_list.js
  53. +82
    -6
      app/javascript/mastodon/components/status.js
  54. +473
    -0
      app/javascript/mastodon/components/status2.js
  55. +7
    -6
      app/javascript/mastodon/components/status_action_bar.js
  56. +1
    -1
      app/javascript/mastodon/components/status_content.js
  57. +2
    -3
      app/javascript/mastodon/components/status_list.js
  58. +2
    -4
      app/javascript/mastodon/containers/mastodon.js
  59. +18
    -0
      app/javascript/mastodon/containers/scroll_container.js
  60. +64
    -4
      app/javascript/mastodon/containers/status_container.js
  61. +168
    -0
      app/javascript/mastodon/containers/status_container2.js
  62. +3
    -4
      app/javascript/mastodon/features/account_gallery/index.js
  63. +1
    -3
      app/javascript/mastodon/features/account_timeline/index.js
  64. +1
    -3
      app/javascript/mastodon/features/blocks/index.js
  65. +1
    -3
      app/javascript/mastodon/features/bookmarked_statuses/index.js
  66. +1
    -3
      app/javascript/mastodon/features/community_timeline/index.js
  67. +2
    -2
      app/javascript/mastodon/features/compose/components/compose_form.js
  68. +1
    -1
      app/javascript/mastodon/features/compose/components/poll_form.js
  69. +1
    -0
      app/javascript/mastodon/features/compose/containers/navigation_container.js
  70. +17
    -1
      app/javascript/mastodon/features/compose/index.js
  71. +0
    -1
      app/javascript/mastodon/features/direct_timeline/components/conversations_list.js
  72. +1
    -3
      app/javascript/mastodon/features/direct_timeline/index.js
  73. +3
    -4
      app/javascript/mastodon/features/directory/index.js
  74. +1
    -3
      app/javascript/mastodon/features/domain_blocks/index.js
  75. +2
    -4
      app/javascript/mastodon/features/favourited_statuses/index.js
  76. +1
    -3
      app/javascript/mastodon/features/favourites/index.js
  77. +1
    -3
      app/javascript/mastodon/features/follow_requests/index.js
  78. +1
    -3
      app/javascript/mastodon/features/followers/index.js
  79. +1
    -3
      app/javascript/mastodon/features/following/index.js
  80. +1
    -1
      app/javascript/mastodon/features/getting_started/components/trends.js
  81. +1
    -1
      app/javascript/mastodon/features/getting_started/index.js
  82. +1
    -3
      app/javascript/mastodon/features/hashtag_timeline/index.js
  83. +1
    -3
      app/javascript/mastodon/features/home_timeline/index.js
  84. +1
    -3
      app/javascript/mastodon/features/list_timeline/index.js
  85. +1
    -2
      app/javascript/mastodon/features/lists/index.js
  86. +1
    -3
      app/javascript/mastodon/features/mutes/index.js
  87. +2
    -2
      app/javascript/mastodon/features/notifications/components/filter_bar.js
  88. +1
    -1
      app/javascript/mastodon/features/notifications/components/notification.js
  89. +1
    -3
      app/javascript/mastodon/features/notifications/index.js
  90. +11
    -6
      app/javascript/mastodon/features/picture_in_picture/components/footer.js
  91. +1
    -3
      app/javascript/mastodon/features/pinned_statuses/index.js
  92. +1
    -3
      app/javascript/mastodon/features/public_timeline/index.js
  93. +1
    -3
      app/javascript/mastodon/features/reblogs/index.js
  94. +9
    -6
      app/javascript/mastodon/features/status/components/action_bar.js
  95. +18
    -5
      app/javascript/mastodon/features/status/components/detailed_status.js
  96. +99
    -16
      app/javascript/mastodon/features/status/index.js
  97. +0
    -27
      app/javascript/mastodon/features/ui/components/audio_modal.js
  98. +6
    -0
      app/javascript/mastodon/features/ui/components/columns_area.js
  99. +8
    -1
      app/javascript/mastodon/features/ui/components/confirmation_modal.js
  100. +2
    -1
      app/javascript/mastodon/features/ui/components/link_footer.js

+ 41
- 1
.circleci/config.yml View File

@ -167,8 +167,45 @@ jobs:
name: Create database
command: ./bin/rails db:create
- run:
name: Run migrations
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all remaining migrations
test-two-step-migrations:
<<: *defaults
docker:
- image: circleci/ruby:2.7-buster-node
environment: *ruby_environment
- image: circleci/postgres:12.2
environment:
POSTGRES_USER: root
POSTGRES_HOST_AUTH_METHOD: trust
- image: circleci/redis:5-alpine
steps:
- *attach_workspace
- *install_system_dependencies
- run:
command: ./bin/rails db:create
name: Create database
- run:
command: ./bin/rails db:migrate VERSION=20171010025614
name: Run migrations up to v2.0.0
- run:
command: ./bin/rails tests:migrations:populate_v2
name: Populate database with test data
- run:
command: ./bin/rails db:migrate
name: Run all pre-deployment migrations
evironment:
SKIP_POST_DEPLOYMENT_MIGRATIONS: true
- run:
command: ./bin/rails db:migrate
name: Run all post-deployment remaining migrations
test-ruby2.7:
<<: *defaults
@ -238,6 +275,9 @@ workflows:
- test-migrations:
requires:
- install-ruby2.7
- test-two-step-migrations:
requires:
- install-ruby2.7
- test-ruby2.7:
requires:
- install-ruby2.7

+ 6
- 0
.env.production.sample View File

@ -4,6 +4,12 @@
# not demonstrate all available configuration options. Please look at
# https://docs.joinmastodon.org/admin/config/ for the full documentation.
# Note that this file accepts slightly different syntax depending on whether
# you are using `docker-compose` or not. In particular, if you use
# `docker-compose`, the value of each declared variable will be taken verbatim,
# including surrounding quotes.
# See: https://github.com/mastodon/mastodon/issues/16895
# Federation
# ----------
# This identifies your server and cannot be changed safely later

+ 1
- 1
.github/CODEOWNERS View File

@ -1,4 +1,4 @@
# CODEOWNERS for tootsuite/mastodon
# CODEOWNERS for mastodon/mastodon
# Translators
# To add translator, copy these lines, replace `fr` with appropriate language code and replace `@żelipapą` with user's GitHub nickname preceded by `@` sign or e-mail address.

+ 34
- 0
.github/workflows/build-image.yml View File

@ -0,0 +1,34 @@
name: Build container image
on:
workflow_dispatch:
push:
branches:
- "main"
tags:
- "*"
jobs:
build-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: docker/setup-buildx-action@v1
- uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: docker/metadata-action@v3
id: meta
with:
images: tootsuite/mastodon
flavor: |
latest=auto
tags: |
type=edge,branch=main
type=semver,pattern={{ raw }}
- uses: docker/build-push-action@v2
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=tootsuite/mastodon:latest
cache-to: type=inline

+ 6
- 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
@ -65,3 +67,7 @@ yarn-debug.log
# Ignore Docker option files
docker-compose.override.yml
# ctag
.ctags
tags

+ 2
- 2
AUTHORS.md View File

@ -1,7 +1,7 @@
Authors
=======
Mastodon is available on [GitHub](https://github.com/tootsuite/mastodon)
Mastodon is available on [GitHub](https://github.com/mastodon/mastodon)
and provided thanks to the work of the following contributors:
* [Gargron](https://github.com/Gargron)
@ -719,7 +719,7 @@ and provided thanks to the work of the following contributors:
* [西小倉宏信](mailto:nishiko@mindia.jp)
* [雨宮美羽](mailto:k737566@gmail.com)
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/tootsuite/mastodon/graphs/contributors) instead.
This document is provided for informational purposes only. Since it is only updated once per release, the version you are looking at may be currently out of date. To see the full list of contributors, consider looking at the [git history](https://github.com/mastodon/mastodon/graphs/contributors) instead.
## Translators

+ 1414
- 1325
CHANGELOG.md
File diff suppressed because it is too large
View File


+ 2
- 2
CONTRIBUTING.md View File

@ -14,7 +14,7 @@ If your contributions are accepted into Mastodon, you can request to be paid thr
## Bug reports
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/tootsuite/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
Bug reports and feature suggestions must use descriptive and concise titles and be submitted to [GitHub Issues](https://github.com/mastodon/mastodon/issues). Please use the search function to make sure that you are not submitting duplicates, and that a similar report or request has not already been resolved or rejected.
## Translations
@ -44,4 +44,4 @@ It is not always possible to phrase every change in such a manner, but it is des
## Documentation
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to tootsuite/documentation](https://github.com/tootsuite/documentation).
The [Mastodon documentation](https://docs.joinmastodon.org) is a statically generated site. You can [submit merge requests to mastodon/documentation](https://github.com/mastodon/documentation).

+ 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元素中的正则限制,也可以前期先不使用邮箱正则功能,还可以直接在服务器上通过命令行创建帐号。

+ 2
- 2
Dockerfile View File

@ -54,8 +54,8 @@ RUN npm install -g yarn && \
COPY Gemfile* package.json yarn.lock /opt/mastodon/
RUN cd /opt/mastodon && \
bundle config set deployment 'true' && \
bundle config set without 'development test' && \
bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install -j"$(nproc)" && \
yarn install --pure-lockfile

+ 3
- 2
Gemfile.lock View File

@ -545,8 +545,9 @@ GEM
rack (>= 1.1)
rubocop (>= 1.7.0, < 2.0)
ruby-progressbar (1.11.0)
ruby-saml (1.11.0)
nokogiri (>= 1.5.10)
ruby-saml (1.13.0)
nokogiri (>= 1.10.5)
rexml
ruby2_keywords (0.0.4)
rufus-scheduler (3.6.0)
fugit (~> 1.1, >= 1.1.6)

+ 4
- 4
README.md View File

@ -1,14 +1,14 @@
![Mastodon](https://i.imgur.com/NhZc40l.png)
========
[![GitHub release](https://img.shields.io/github/release/tootsuite/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/tootsuite/mastodon.svg)][circleci]
[![GitHub release](https://img.shields.io/github/release/mastodon/mastodon.svg)][releases]
[![Build Status](https://img.shields.io/circleci/project/github/mastodon/mastodon.svg)][circleci]
[![Code Climate](https://img.shields.io/codeclimate/maintainability/tootsuite/mastodon.svg)][code_climate]
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/mastodon/localized.svg)][crowdin]
[![Docker Pulls](https://img.shields.io/docker/pulls/tootsuite/mastodon.svg)][docker]
[releases]: https://github.com/tootsuite/mastodon/releases
[circleci]: https://circleci.com/gh/tootsuite/mastodon
[releases]: https://github.com/mastodon/mastodon/releases
[circleci]: https://circleci.com/gh/mastodon/mastodon
[code_climate]: https://codeclimate.com/github/tootsuite/mastodon
[crowdin]: https://crowdin.com/project/mastodon
[docker]: https://hub.docker.com/r/tootsuite/mastodon/

+ 2
- 2
app.json View File

@ -1,8 +1,8 @@
{
"name": "Mastodon",
"description": "A GNU Social-compatible microblogging server",
"repository": "https://github.com/tootsuite/mastodon",
"logo": "https://github.com/tootsuite.png",
"repository": "https://github.com/mastodon/mastodon",
"logo": "https://github.com/mastodon.png",
"env": {
"HEROKU": {
"description": "Leave this as true",

+ 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]
@ -28,6 +29,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/activitypub/followers_synchronizations_controller.rb View File

@ -19,11 +19,11 @@ class ActivityPub::FollowersSynchronizationsController < ActivityPub::BaseContro
private
def uri_prefix
signed_request_account.uri[/http(s?):\/\/[^\/]+\//]
signed_request_account.uri[Account::URL_PREFIX_RE]
end
def set_items
@items = @account.followers.where(Account.arel_table[:uri].matches(uri_prefix + '%';, false, true)).pluck(:uri)
@items = @account.followers.where(Account.arel_table[:uri].matches("#{Account.sanitize_sql_like(uri_prefix)}/%";, false, true)).or(@account.followers.where(uri: uri_prefix)).pluck(:uri)
end
def collection_presenter

+ 9
- 1
app/controllers/activitypub/outboxes_controller.rb View File

@ -11,7 +11,11 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
before_action :set_cache_headers
def show
expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode? && !(signed_request_account.present? && page_requested?))
if page_requested?
expires_in(1.minute, public: public_fetch_mode? && signed_request_account.nil?)
else
expires_in(3.minutes, public: public_fetch_mode?)
end
render json: outbox_presenter, serializer: ActivityPub::OutboxSerializer, adapter: ActivityPub::Adapter, content_type: 'application/activity+json'
end
@ -76,4 +80,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
def set_account
@account = params[:account_username].present? ? Account.find_local!(username_param) : Account.representative
end
def set_cache_headers
response.headers['Vary'] = 'Signature' if authorized_fetch_mode? || page_requested?
end
end

+ 1
- 1
app/controllers/admin/statuses_controller.rb View File

@ -14,7 +14,7 @@ module Admin
@statuses = @account.statuses.where(visibility: [:public, :unlisted])
if params[:media]
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id))
@statuses.merge!(Status.joins(:media_attachments).merge(@account.media_attachments.reorder(nil)).group(:id)).reorder('statuses.id desc')
end
@statuses = @statuses.preload(:media_attachments, :mentions).page(params[:page]).per(PER_PAGE)

+ 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].strip.end_with?(anon.tag) && generate_anon_name(current_user.account.username + anon.salt + (Time.now - 5.hours).strftime("%D"), anon.namelist, Account.find(anon.acc).note)
sender = anon_name ? Account.find(anon.acc) : current_user.account
st_text = anon_name ? ("[#{anon_name}]:\n#{status_params[:status]}"[0..5000]) : status_params[:status]
@status = PostStatusService.new.call(sender,
text: st_text,
thread: @thread,
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

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

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

+ 0
- 1
app/controllers/auth/passwords_controller.rb View File

@ -10,7 +10,6 @@ class Auth::PasswordsController < Devise::PasswordsController
super do |resource|
if resource.errors.empty?
resource.session_activations.destroy_all
resource.forget_me!
end
end
end

+ 0
- 3
app/controllers/auth/registrations_controller.rb View File

@ -1,7 +1,6 @@
# frozen_string_literal: true
class Auth::RegistrationsController < Devise::RegistrationsController
include Devise::Controllers::Rememberable
include RegistrationSpamConcern
layout :determine_layout
@ -30,8 +29,6 @@ class Auth::RegistrationsController < Devise::RegistrationsController
super do |resource|
if resource.saved_change_to_encrypted_password?
resource.clear_other_sessions(current_session.session_id)
resource.forget_me!
remember_me(resource)
end
end
end

+ 11
- 10
app/controllers/auth/sessions_controller.rb View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Auth::SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable
layout 'auth'
skip_before_action :require_no_authentication, only: [:create]
@ -26,7 +24,6 @@ class Auth::SessionsController < Devise::SessionsController
def create
super do |resource|
resource.update_sign_in!(request, new_sign_in: true)
remember_me(resource)
flash.delete(:notice)
end
end
@ -40,7 +37,7 @@ class Auth::SessionsController < Devise::SessionsController
end
def webauthn_options
user = find_user
user = User.find_by(id: session[:attempt_user_id])
if user.webauthn_enabled?
options_for_get = WebAuthn::Credential.options_for_get(
@ -58,16 +55,20 @@ class Auth::SessionsController < Devise::SessionsController
protected
def find_user
if session[:attempt_user_id]
if user_params[:email].present?
find_user_from_params
elsif session[:attempt_user_id]
User.find_by(id: session[:attempt_user_id])
else
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
end
def find_user_from_params
user = User.authenticate_with_ldap(user_params) if Devise.ldap_authentication
user ||= User.authenticate_with_pam(user_params) if Devise.pam_authentication
user ||= User.find_for_authentication(email: user_params[:email])
user
end
def user_params
params.require(:user).permit(:email, :password, :otp_attempt, :sign_in_token_attempt, credential: {})
end

+ 12
- 9
app/controllers/concerns/sign_in_token_authentication_concern.rb View File

@ -16,21 +16,24 @@ module SignInTokenAuthenticationConcern
end
def authenticate_with_sign_in_token
user = self.resource = find_user
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user_params.key?(:sign_in_token_attempt) && session[:attempt_user_id]
authenticate_with_sign_in_token_attempt(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_sign_in_token(user)
if user_params[:email].present?
user = self.resource = find_user_from_params
prompt_for_sign_in_token(user) if user&.external_or_valid_password?(user_params[:password])
elsif session[:attempt_user_id]
user = self.resource = User.find_by(id: session[:attempt_user_id])
return if user.nil?
if session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user_params.key?(:sign_in_token_attempt)
authenticate_with_sign_in_token_attempt(user)
end
end
end
def authenticate_with_sign_in_token_attempt(user)
if valid_sign_in_token_attempt?(user)
clear_attempt_from_session
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_sign_in_token')

+ 13
- 11
app/controllers/concerns/two_factor_authentication_concern.rb View File

@ -35,16 +35,20 @@ module TwoFactorAuthenticationConcern
end
def authenticate_with_two_factor
user = self.resource = find_user
if user_params[:email].present?
user = self.resource = find_user_from_params
prompt_for_two_factor(user) if user&.external_or_valid_password?(user_params[:password])
elsif session[:attempt_user_id]
user = self.resource = User.find_by(id: session[:attempt_user_id])
return if user.nil?
if user.present? && session[:attempt_user_id].present? && session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user.webauthn_enabled? && user_params.key?(:credential) && session[:attempt_user_id]
authenticate_with_two_factor_via_webauthn(user)
elsif user_params.key?(:otp_attempt) && session[:attempt_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user.present? && user.external_or_valid_password?(user_params[:password])
prompt_for_two_factor(user)
if session[:attempt_user_updated_at] != user.updated_at.to_s
restart_session
elsif user.webauthn_enabled? && user_params.key?(:credential)
authenticate_with_two_factor_via_webauthn(user)
elsif user_params.key?(:otp_attempt)
authenticate_with_two_factor_via_otp(user)
end
end
end
@ -53,7 +57,6 @@ module TwoFactorAuthenticationConcern
if valid_webauthn_credential?(user, webauthn_credential)
clear_attempt_from_session
remember_me(user)
sign_in(user)
render json: { redirect_path: root_path }, status: :ok
else
@ -64,7 +67,6 @@ module TwoFactorAuthenticationConcern
def authenticate_with_two_factor_via_otp(user)
if valid_otp_attempt?(user)
clear_attempt_from_session
remember_me(user)
sign_in(user)
else
flash.now[:alert] = I18n.t('users.invalid_otp_token')

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

@ -85,7 +85,7 @@ class FollowerAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
%i(id type total_items)
end
end
end

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

@ -85,7 +85,7 @@ class FollowingAccountsController < ApplicationController
if page_requested? || !@account.user_hides_network?
# Return all fields
else
%i(id type totalItems)
%i(id type total_items)
end
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)

+ 1
- 1
app/controllers/settings/deletes_controller.rb View File

@ -42,7 +42,7 @@ class Settings::DeletesController < Settings::BaseController
end
def destroy_account!
current_account.suspend!(origin: :local)
current_account.suspend!(origin: :local, block_email: false)
AccountDeletionWorker.perform_async(current_user.account_id)
sign_out
end

+ 2
- 1
app/controllers/well_known/webfinger_controller.rb View File

@ -4,7 +4,6 @@ module WellKnown
class WebfingerController < ActionController::Base
include RoutingHelper
before_action { response.headers['Vary'] = 'Accept' }
before_action :set_account
before_action :check_account_suspension
@ -39,10 +38,12 @@ module WellKnown
end
def bad_request
expires_in(3.minutes, public: true)
head 400
end
def not_found
expires_in(3.minutes, public: true)
head 404
end

+ 3
- 3
app/helpers/accounts_helper.rb View File

@ -80,17 +80,17 @@ module AccountsHelper
def account_description(account)
prepend_str = [
[
number_to_human(account.statuses_count, strip_insignificant_zeros: true),
number_to_human(account.statuses_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.posts', count: account.statuses_count),
].join(' '),
[
number_to_human(account.following_count, strip_insignificant_zeros: true),
number_to_human(account.following_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.following', count: account.following_count),
].join(' '),
[
number_to_human(account.followers_count, strip_insignificant_zeros: true),
number_to_human(account.followers_count, precision: 3, strip_insignificant_zeros: true),
I18n.t('accounts.followers', count: account.followers_count),
].join(' '),
].join(', ')

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

@ -14,6 +14,17 @@ module ApplicationHelper
ku
).freeze
def friendly_number_to_human(number, **options)
# By default, the number of precision digits used by number_to_human
# is looked up from the locales definition, and rails-i18n comes with
# values that don't seem to make much sense for many languages, so
# override these values with a default of 3 digits of precision.
options[:precision] = 3
options[:strip_insignificant_zeros] = true
number_to_human(number, **options)
end
def active_nav_class(*paths)
paths.any? { |path| current_page?(path) } ? 'active' : ''
end
@ -40,7 +51,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
@ -76,6 +88,11 @@ module ApplicationHelper
policy(record).public_send("#{action}?")
end
def masked_email(email)
email_username = email.split('@').first
"#{email_username[0]}***#{email_username[-1]}@#{email.split('@').last}"
end
def fa_icon(icon, attributes = {})
class_names = attributes[:class]&.split(' ') || []
class_names << 'fa'

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

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

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

@ -7,7 +7,7 @@ module HomeHelper
}
end
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

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

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

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

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

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

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

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


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

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

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

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

BIN
View File


BIN
View File


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

@ -60,8 +60,11 @@ export function unreblog(status) {
return (dispatch, getState) => {
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));

+ 14
- 7
app/javascript/mastodon/actions/picture_in_picture.js View File

@ -22,13 +22,20 @@ export const PICTURE_IN_PICTURE_REMOVE = 'PICTURE_IN_PICTURE_REMOVE';
* @param {MediaProps} props
* @return {object}
*/
export const deployPictureInPicture = (statusId, accountId, playerType, props) => ({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
export const deployPictureInPicture = (statusId, accountId, playerType, props) => {
return (dispatch, getState) => {
// Do not open a player for a toot that does not exist
if (getState().hasIn(['statuses', statusId])) {
dispatch({
type: PICTURE_IN_PICTURE_DEPLOY,
statusId,
accountId,
playerType,
props,
});
}
};
};
/*
* @return {object}

+ 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';
@ -112,6 +112,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());

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

@ -93,7 +93,7 @@ export default class IntersectionObserverArticle extends React.Component {
// When the browser gets a chance, test if we're still not intersecting,
// and if so, set our isHidden to true to trigger an unrender. The point of
// this is to save DOM nodes and avoid using up too much memory.
// See: https://github.com/tootsuite/mastodon/issues/2900
// See: https://github.com/mastodon/mastodon/issues/2900
this.setState((prevState) => ({ isHidden: !prevState.isIntersecting }));
}

+ 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}

+ 40
- 0
app/javascript/mastodon/components/modal_root.js View File

@ -1,10 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import 'wicg-inert';
import { createBrowserHistory } from 'history';
import { multiply } from 'color-blend';
export default class ModalRoot extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,
};
static propTypes = {
children: PropTypes.node,
onClose: PropTypes.func.isRequired,
@ -48,6 +53,7 @@ export default class ModalRoot extends React.PureComponent {
componentDidMount () {
window.addEventListener('keyup', this.handleKeyUp, false);
window.addEventListener('keydown', this.handleKeyDown, false);
this.history = this.context.router ? this.context.router.history : createBrowserHistory();
}
componentWillReceiveProps (nextProps) {
@ -69,6 +75,14 @@ export default class ModalRoot extends React.PureComponent {
this.activeElement.focus({ preventScroll: true });
this.activeElement = null;
}).catch(console.error);
this._handleModalClose();
}
if (this.props.children && !prevProps.children) {
this._handleModalOpen();
}
if (this.props.children) {
this._ensureHistoryBuffer();
}
}
@ -77,6 +91,32 @@ export default class ModalRoot extends React.PureComponent {
window.removeEventListener('keydown', this.handleKeyDown);
}
_handleModalOpen () {
this._modalHistoryKey = Date.now();
this.unlistenHistory = this.history.listen((_, action) => {
if (action === 'POP') {
this.props.onClose();
}
});
}
_handleModalClose () {
if (this.unlistenHistory) {
this.unlistenHistory();
}
const { state } = this.history.location;
if (state && state.mastodonModalKey === this._modalHistoryKey) {
this.history.goBack();
}
}
_ensureHistoryBuffer () {
const { pathname, state } = this.history.location;
if (!state || state.mastodonModalKey !== this._modalHistoryKey) {
this.history.push(pathname, { ...state, mastodonModalKey: this._modalHistoryKey });
}
}
getSiblings = () => {
return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node);
}

+ 3
- 4
app/javascript/mastodon/components/scrollable_list.js View File

@ -1,5 +1,5 @@
import React, { PureComponent } from 'react';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
import PropTypes from 'prop-types';
import IntersectionObserverArticleContainer from '../containers/intersection_observer_article_container';
import LoadMore from './load_more';
@ -34,7 +34,6 @@ class ScrollableList extends PureComponent {
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
showLoading: PropTypes.bool,
hasMore: PropTypes.bool,
@ -290,7 +289,7 @@ class ScrollableList extends PureComponent {
}
render () {
const { children, scrollKey, trackScroll, shouldUpdateScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
const { children, scrollKey, trackScroll, showLoading, isLoading, hasMore, numPending, prepend, alwaysPrepend, append, emptyMessage, onLoadMore } = this.props;
const { fullscreen } = this.state;
const childrenCount = React.Children.count(children);
@ -356,7 +355,7 @@ class ScrollableList extends PureComponent {
if (trackScroll) {
return (
<ScrollContainer scrollKey={scrollKey} shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey={scrollKey}>
{scrollableArea}
</ScrollContainer>
);

+ 82
- 6
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;
@ -309,8 +344,8 @@ class Status extends ImmutablePureComponent {
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')}
<span>{status.getIn(['account', 'display_name']) || status.getIn(['account', 'username'])}</span>
<span>{status.get('content')}</span>
</div>
</HotKeys>
);
@ -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' },
@ -297,11 +298,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';
@ -323,9 +324,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

@ -172,7 +172,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') };

+ 2
- 3
app/javascript/mastodon/components/status_list.js View File

@ -18,7 +18,6 @@ export default class StatusList extends ImmutablePureComponent {
onScrollToTop: PropTypes.func,
onScroll: PropTypes.func,
trackScroll: PropTypes.bool,
shouldUpdateScroll: PropTypes.func,
isLoading: PropTypes.bool,
isPartial: PropTypes.bool,
hasMore: PropTypes.bool,
@ -77,7 +76,7 @@ export default class StatusList extends ImmutablePureComponent {
}
render () {
const { statusIds, featuredStatusIds, shouldUpdateScroll, onLoadMore, timelineId, ...other } = this.props;
const { statusIds, featuredStatusIds, onLoadMore, timelineId, ...other } = this.props;
const { isLoading, isPartial } = other;
if (isPartial) {
@ -120,7 +119,7 @@ export default class StatusList extends ImmutablePureComponent {
}
return (
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} shouldUpdateScroll={shouldUpdateScroll} ref={this.setRef}>
<ScrollableList {...other} showLoading={isLoading && statusIds.size === 0} onLoadMore={onLoadMore && this.handleLoadOlder} ref={this.setRef}>
{scrollableContent}
</ScrollableList>
);

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

@ -10,8 +10,6 @@ import { hydrateStore } from '../actions/store';
import { connectUserStream } from '../actions/streaming';
import { IntlProvider, addLocaleData } from 'react-intl';
import { getLocale } from '../locales';
import { previewState as previewMediaState } from 'mastodon/features/ui/components/media_modal';
import { previewState as previewVideoState } from 'mastodon/features/ui/components/video_modal';
import initialState from '../initial_state';
import ErrorBoundary from '../components/error_boundary';
@ -41,8 +39,8 @@ export default class Mastodon extends React.PureComponent {
}
}
shouldUpdateScroll (_, { location }) {
return location.state !== previewMediaState && location.state !== previewVideoState;
shouldUpdateScroll (prevRouterProps, { location }) {
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
}
render () {

+ 18
- 0
app/javascript/mastodon/containers/scroll_container.js View File

@ -0,0 +1,18 @@
import { ScrollContainer as OriginalScrollContainer } from 'react-router-scroll-4';
// ScrollContainer is used to automatically scroll to the top when pushing a
// new history state and remembering the scroll position when going back.
// There are a few things we need to do differently, though.
const defaultShouldUpdateScroll = (prevRouterProps, { location }) => {
// If the change is caused by opening a modal, do not scroll to top
return !(location.state?.mastodonModalKey && location.state?.mastodonModalKey !== prevRouterProps?.location?.state?.mastodonModalKey);
};
export default
class ScrollContainer extends OriginalScrollContainer {
static defaultProps = {
shouldUpdateScroll: defaultShouldUpdateScroll,
};
}

+ 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 {
@ -40,7 +42,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({
@ -57,11 +59,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 && treeRoot && 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));

+ 3
- 4
app/javascript/mastodon/features/account_gallery/index.js View File

@ -11,7 +11,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import { getAccountGallery } from 'mastodon/selectors';
import MediaItem from './components/media_item';
import HeaderContainer from '../account_timeline/containers/header_container';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
import LoadMore from 'mastodon/components/load_more';
import MissingIndicator from 'mastodon/components/missing_indicator';
import { openModal } from 'mastodon/actions/modal';
@ -29,7 +29,6 @@ const mapStateToProps = (state, props) => ({
class LoadMoreMedia extends ImmutablePureComponent {
static propTypes = {
shouldUpdateScroll: PropTypes.func,
maxId: PropTypes.string,
onLoadMore: PropTypes.func.isRequired,
};
@ -127,7 +126,7 @@ class AccountGallery extends ImmutablePureComponent {
}
render () {
const { attachments, shouldUpdateScroll, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;
const { width } = this.state;
if (!isAccount) {
@ -164,7 +163,7 @@ class AccountGallery extends ImmutablePureComponent {
<Column>
<ColumnBackButton multiColumn={multiColumn} />
<ScrollContainer scrollKey='account_gallery' shouldUpdateScroll={shouldUpdateScroll}>
<ScrollContainer scrollKey='account_gallery'>
<div className='scrollable scrollable--flex' onScroll={this.handleScroll}>
<HeaderContainer accountId={this.props.params.accountId} />

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

@ -50,7 +50,6 @@ class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list,
featuredStatusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
@ -115,7 +114,7 @@ class AccountTimeline extends ImmutablePureComponent {
}
render () {
const { shouldUpdateScroll, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
const { statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, multiColumn, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -162,7 +161,6 @@ class AccountTimeline extends ImmutablePureComponent {
isLoading={isLoading}
hasMore={hasMore}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='account'

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

@ -29,7 +29,6 @@ class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
@ -46,7 +45,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, accountIds, shouldUpdateScroll, hasMore, multiColumn, isLoading } = this.props;
const { intl, accountIds, hasMore, multiColumn, isLoading } = this.props;
if (!accountIds) {
return (
@ -66,7 +65,6 @@ class Blocks extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

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

@ -27,7 +27,6 @@ class Bookmarks extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
@ -68,7 +67,7 @@ class Bookmarks extends ImmutablePureComponent {
}, 300, { leading: true })
render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.bookmarked_statuses' defaultMessage="You don't have any bookmarked toots yet. When you bookmark one, it will show up here." />;
@ -93,7 +92,6 @@ class Bookmarks extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>

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

@ -41,7 +41,6 @@ class CommunityTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
@ -103,7 +102,7 @@ class CommunityTimeline extends React.PureComponent {
}
render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props;
const pinned = !!columnId;
return (
@ -127,7 +126,6 @@ class CommunityTimeline extends React.PureComponent {
timelineId={`community${onlyMedia ? ':media' : ''}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

+ 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 = () => {
@ -257,7 +257,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'>

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

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

+ 1
- 0
app/javascript/mastodon/features/compose/containers/navigation_container.js View File

@ -21,6 +21,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
},

+ 17
- 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;
@ -74,6 +81,7 @@ class Compose extends React.PureComponent {
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
@ -87,9 +95,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 +114,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') && treeRoot && (
<Link to={treeRoot} className='drawer__tab' title={intl.formatMessage(messages.tree)} aria-label={intl.formatMessage(messages.tree)}><Icon id='tree' fixedWidth /></Link>
)}
{!columns.some(column => column.get('id') === 'NOTIFICATIONS') && (
<Link to='/notifications' className='drawer__tab' title={intl.formatMessage(messages.notifications)} aria-label={intl.formatMessage(messages.notifications)}><Icon id='bell' fixedWidth /></Link>
)}

+ 0
- 1
app/javascript/mastodon/features/direct_timeline/components/conversations_list.js View File

@ -14,7 +14,6 @@ export default class ConversationsList extends ImmutablePureComponent {
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
onLoadMore: PropTypes.func,
shouldUpdateScroll: PropTypes.func,
};
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)

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

@ -19,7 +19,6 @@ class DirectTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
@ -71,7 +70,7 @@ class DirectTimeline extends React.PureComponent {
}
render () {
const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props;
const { intl, hasUnread, columnId, multiColumn } = this.props;
const pinned = !!columnId;
return (
@ -93,7 +92,6 @@ class DirectTimeline extends React.PureComponent {
timelineId='direct'
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any direct messages yet. When you send or receive one, it will show up here." />}
shouldUpdateScroll={shouldUpdateScroll}
/>
</Column>
);

+ 3
- 4
app/javascript/mastodon/features/directory/index.js View File

@ -12,7 +12,7 @@ import AccountCard from './components/account_card';
import RadioButton from 'mastodon/components/radio_button';
import classNames from 'classnames';
import LoadMore from 'mastodon/components/load_more';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
const messages = defineMessages({
title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
@ -40,7 +40,6 @@ class Directory extends React.PureComponent {
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
intl: PropTypes.object.isRequired,
multiColumn: PropTypes.bool,
@ -125,7 +124,7 @@ class Directory extends React.PureComponent {
}
render () {
const { isLoading, accountIds, intl, columnId, multiColumn, domain, shouldUpdateScroll } = this.props;
const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;
const { order, local } = this.getParams(this.props, this.state);
const pinned = !!columnId;
@ -163,7 +162,7 @@ class Directory extends React.PureComponent {
multiColumn={multiColumn}
/>
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory' shouldUpdateScroll={shouldUpdateScroll}>{scrollableArea}</ScrollContainer> : scrollableArea}
{multiColumn && !pinned ? <ScrollContainer scrollKey='directory'>{scrollableArea}</ScrollContainer> : scrollableArea}
</Column>
);
}

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

@ -29,7 +29,6 @@ class Blocks extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
domains: ImmutablePropTypes.orderedSet,
intl: PropTypes.object.isRequired,
@ -45,7 +44,7 @@ class Blocks extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, domains, shouldUpdateScroll, hasMore, multiColumn } = this.props;
const { intl, domains, hasMore, multiColumn } = this.props;
if (!domains) {
return (
@ -64,7 +63,6 @@ class Blocks extends ImmutablePureComponent {
scrollKey='domain_blocks'
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

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

@ -27,7 +27,6 @@ class Favourites extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
@ -68,7 +67,7 @@ class Favourites extends ImmutablePureComponent {
}, 300, { leading: true })
render () {
const { intl, shouldUpdateScroll, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.favourited_statuses' defaultMessage="You don't have any favourite toots yet. When you favourite one, it will show up here." />;
@ -76,7 +75,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}
@ -93,7 +92,6 @@ class Favourites extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
/>

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

@ -27,7 +27,6 @@ class Favourites extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
@ -50,7 +49,7 @@ class Favourites extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
const { intl, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -74,7 +73,6 @@ class Favourites extends ImmutablePureComponent {
<ScrollableList
scrollKey='favourites'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

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

@ -32,7 +32,6 @@ class FollowRequests extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
@ -51,7 +50,7 @@ class FollowRequests extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
const { intl, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
if (!accountIds) {
return (
@ -80,7 +79,6 @@ class FollowRequests extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
prepend={unlockedPrependMessage}

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

@ -43,7 +43,6 @@ class Followers extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
@ -73,7 +72,7 @@ class Followers extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -112,7 +111,6 @@ class Followers extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}

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

@ -43,7 +43,6 @@ class Following extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
@ -73,7 +72,7 @@ class Following extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { shouldUpdateScroll, accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
const { accountIds, hasMore, blockedBy, isAccount, multiColumn, isLoading, remote, remoteUrl } = this.props;
if (!isAccount) {
return (
@ -112,7 +111,6 @@ class Following extends ImmutablePureComponent {
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
shouldUpdateScroll={shouldUpdateScroll}
prepend={<HeaderContainer accountId={this.props.params.accountId} hideTabs />}
alwaysPrepend
append={remoteMessage}

+ 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' />,
);

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

@ -24,7 +24,6 @@ class HashtagTimeline extends React.PureComponent {
params: PropTypes.object.isRequired,
columnId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
};
@ -130,7 +129,7 @@ class HashtagTimeline extends React.PureComponent {
}
render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn } = this.props;
const { hasUnread, columnId, multiColumn } = this.props;
const { id, local } = this.props.params;
const pinned = !!columnId;
@ -156,7 +155,6 @@ class HashtagTimeline extends React.PureComponent {
timelineId={`hashtag:${id}${local ? ':local' : ''}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

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

@ -34,7 +34,6 @@ class HomeTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
hasUnread: PropTypes.bool,
isPartial: PropTypes.bool,
@ -112,7 +111,7 @@ class HomeTimeline extends React.PureComponent {
}
render () {
const { intl, shouldUpdateScroll, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;
const pinned = !!columnId;
let announcementsButton = null;
@ -154,7 +153,6 @@ class HomeTimeline extends React.PureComponent {
onLoadMore={this.handleLoadMore}
timelineId='home'
emptyMessage={<FormattedMessage id='empty_column.home' defaultMessage='Your home timeline is empty! Follow more people to fill it up. {suggestions}' values={{ suggestions: <Link to='/start'><FormattedMessage id='empty_column.home.suggestions' defaultMessage='See some suggestions' /></Link> }} />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

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

@ -41,7 +41,6 @@ class ListTimeline extends React.PureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
columnId: PropTypes.string,
hasUnread: PropTypes.bool,
multiColumn: PropTypes.bool,
@ -142,7 +141,7 @@ class ListTimeline extends React.PureComponent {
}
render () {
const { shouldUpdateScroll, hasUnread, columnId, multiColumn, list, intl } = this.props;
const { hasUnread, columnId, multiColumn, list, intl } = this.props;
const { id } = this.props.params;
const pinned = !!columnId;
const title = list ? list.get('title') : id;
@ -207,7 +206,6 @@ class ListTimeline extends React.PureComponent {
timelineId={`list:${id}`}
onLoadMore={this.handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.list' defaultMessage='There is nothing in this list yet. When members of this list post new statuses, they will appear here.' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

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

@ -48,7 +48,7 @@ class Lists extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, lists, multiColumn } = this.props;
const { intl, lists, multiColumn } = this.props;
if (!lists) {
return (
@ -68,7 +68,6 @@ class Lists extends ImmutablePureComponent {
<ScrollableList
scrollKey='lists'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
prepend={<ColumnSubheading text={intl.formatMessage(messages.subheading)} />}
bindToDocument={!multiColumn}

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

@ -29,7 +29,6 @@ class Mutes extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
hasMore: PropTypes.bool,
isLoading: PropTypes.bool,
accountIds: ImmutablePropTypes.list,
@ -46,7 +45,7 @@ class Mutes extends ImmutablePureComponent {
}, 300, { leading: true });
render () {
const { intl, shouldUpdateScroll, hasMore, accountIds, multiColumn, isLoading } = this.props;
const { intl, hasMore, accountIds, multiColumn, isLoading } = this.props;
if (!accountIds) {
return (
@ -66,7 +65,6 @@ class Mutes extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

+ 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')}>

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

@ -74,7 +74,6 @@ class Notifications extends React.PureComponent {
notifications: ImmutablePropTypes.list.isRequired,
showFilterBar: PropTypes.bool.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
isLoading: PropTypes.bool,
isUnread: PropTypes.bool,
@ -176,7 +175,7 @@ class Notifications extends React.PureComponent {
};
render () {
const { intl, notifications, shouldUpdateScroll, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const { intl, notifications, isLoading, isUnread, columnId, multiColumn, hasMore, numPending, showFilterBar, lastReadId, canMarkAsRead, needsNotificationPermission } = this.props;
const pinned = !!columnId;
const emptyMessage = <FormattedMessage id='empty_column.notifications' defaultMessage="You don't have any notifications yet. When other people interact with you, you will see it here." />;
@ -227,7 +226,6 @@ class Notifications extends React.PureComponent {
onLoadPending={this.handleLoadPending}
onScrollToTop={this.handleScrollToTop}
onScroll={this.handleScroll}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
>
{scrollableContent}

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

@ -16,6 +16,7 @@ import { openModal } from 'mastodon/actions/modal';
const messages = defineMessages({
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' },
@ -114,7 +115,11 @@ class Footer extends ImmutablePureComponent {
return;
}
const { status } = this.props;
const { status, onClose } = this.props;
if (onClose) {
onClose();
}
router.history.push(`/statuses/${status.get('id')}`);
}
@ -128,11 +133,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 = '';
@ -149,9 +154,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>
);

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

@ -24,7 +24,6 @@ class PinnedStatuses extends ImmutablePureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
statusIds: ImmutablePropTypes.list.isRequired,
intl: PropTypes.object.isRequired,
hasMore: PropTypes.bool.isRequired,
@ -44,7 +43,7 @@ class PinnedStatuses extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, statusIds, hasMore, multiColumn } = this.props;
const { intl, statusIds, hasMore, multiColumn } = this.props;
return (
<Column bindToDocument={!multiColumn} icon='thumb-tack' heading={intl.formatMessage(messages.heading)} ref={this.setRef}>
@ -53,7 +52,6 @@ class PinnedStatuses extends ImmutablePureComponent {
statusIds={statusIds}
scrollKey='pinned_statuses'
hasMore={hasMore}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

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

@ -43,7 +43,6 @@ class PublicTimeline extends React.PureComponent {
static propTypes = {
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
intl: PropTypes.object.isRequired,
columnId: PropTypes.string,
multiColumn: PropTypes.bool,
@ -106,7 +105,7 @@ class PublicTimeline extends React.PureComponent {
}
render () {
const { intl, shouldUpdateScroll, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;
const pinned = !!columnId;
return (
@ -130,7 +129,6 @@ class PublicTimeline extends React.PureComponent {
trackScroll={!pinned}
scrollKey={`public_timeline-${columnId}`}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
shouldUpdateScroll={shouldUpdateScroll}
bindToDocument={!multiColumn}
/>
</Column>

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

@ -27,7 +27,6 @@ class Reblogs extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
shouldUpdateScroll: PropTypes.func,
accountIds: ImmutablePropTypes.list,
multiColumn: PropTypes.bool,
intl: PropTypes.object.isRequired,
@ -50,7 +49,7 @@ class Reblogs extends ImmutablePureComponent {
}
render () {
const { intl, shouldUpdateScroll, accountIds, multiColumn } = this.props;
const { intl, accountIds, multiColumn } = this.props;
if (!accountIds) {
return (
@ -74,7 +73,6 @@ class Reblogs extends ImmutablePureComponent {
<ScrollableList
scrollKey='reblogs'
shouldUpdateScroll={shouldUpdateScroll}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>

+ 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' },
@ -252,11 +253,13 @@ class ActionBar extends React.PureComponent {
<div className='detailed-status__button'><IconButton title={intl.formatMessage(messages.share)} icon='share-alt' onClick={this.handleShare} /></div>
);
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';
@ -274,9 +277,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>

+ 99
- 16
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,
@ -45,7 +46,7 @@ import { initBlockModal } from '../../actions/blocks';
import { initBoostModal } from '../../actions/boosts';
import { initReport } from '../../actions/reports';
import { makeGetStatus, makeGetPictureInPicture } from '../../selectors';
import { ScrollContainer } from 'react-router-scroll-4';
import ScrollContainer from 'mastodon/containers/scroll_container';
import ColumnBackButton from '../../components/column_back_button';
import ColumnHeader from '../../components/column_header';
import StatusContainer from '../../containers/status_container';
@ -53,7 +54,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';
@ -83,7 +84,7 @@ const makeMapStateToProps = () => {
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id) {
while (id && !mutable.includes(id)) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
@ -101,7 +102,7 @@ const makeMapStateToProps = () => {
const ids = [statusId];
while (ids.length > 0) {
let id = ids.shift();
let id = ids.pop();
const replies = contextReplies.get(id);
if (statusId !== id) {
@ -110,7 +111,7 @@ const makeMapStateToProps = () => {
if (replies) {
replies.reverse().forEach(reply => {
ids.unshift(reply);
if (!ids.includes(reply) && !descendantsIds.includes(reply) && statusId !== reply) ids.push(reply);
});
}
}
@ -128,22 +129,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 }),
@ -181,6 +220,9 @@ class Status extends ImmutablePureComponent {
fullscreen: false,
showMedia: defaultMediaVisibility(this.props.status),
loadedStatusId: undefined,
showTree: false,
svgWidth: 400,
activeNode: null
};
componentWillMount () {
@ -334,6 +376,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');
@ -455,14 +504,17 @@ class Status extends ImmutablePureComponent {
}
}
renderChildren (list) {
return list.map(id => (
renderChildren (list, type) {
const { deep } = this.props;
return list.map((id,idx) => (
<StatusContainer
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}
/>
));
}
@ -496,10 +548,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 { status, deep, ancestorsIds, descendantsIds, treeData, intl, domain, multiColumn, pictureInPicture } = this.props;
const { fullscreen, showTree, svgWidth, activeNode } = this.state;
if (status === null) {
return (
@ -511,11 +572,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 = {
@ -537,19 +598,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}>
<ScrollContainer scrollKey='thread'>
<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}
@ -558,6 +640,7 @@ class Status extends ImmutablePureComponent {
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
/>
}
<ActionBar
key={`action-bar-${status.get('id')}`}

+ 0
- 27
app/javascript/mastodon/features/ui/components/audio_modal.js View File

@ -4,7 +4,6 @@ import PropTypes from 'prop-types';
import Audio from 'mastodon/features/audio';
import { connect } from 'react-redux';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { previewState } from './video_modal';
import Footer from 'mastodon/features/picture_in_picture/components/footer';
const mapStateToProps = (state, { statusId }) => ({
@ -25,32 +24,6 @@ class AudioModal extends ImmutablePureComponent {
onChangeBackgroundColor: PropTypes.func.isRequired,
};
static contextTypes = {
router: PropTypes.object,
};
componentDidMount () {
if (this.context.router) {
const history = this.context.router.history;
history.push(history.location.pathname, previewState);
this.unlistenHistory = history.listen(() => {
this.props.onClose();
});
}
}
componentWillUnmount () {
if (this.context.router) {
this.unlistenHistory();
if (this.context.router.history.location.state === previewState) {
this.context.router.history.goBack();
}
}
}
render () {
const { media, accountStaticAvatar, statusId, onClose } = this.props;
const options = this.props.options || {};

+ 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,
@ -196,6 +198,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>
);

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

@ -13,15 +13,22 @@ class ConfirmationModal extends React.PureComponent {
onConfirm: PropTypes.func.isRequired,
secondary: PropTypes.string,
onSecondary: PropTypes.func,
closeWhenConfirm: PropTypes.bool,
intl: PropTypes.object.isRequired,
};
static defaultProps = {
closeWhenConfirm: true,
};
componentDidMount() {
this.button.focus();
}
handleClick = () => {
this.props.onClose();
if (this.props.closeWhenConfirm) {
this.props.onClose();
}
this.props.onConfirm();
}

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

@ -17,6 +17,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
dispatch(openModal('CONFIRM', {
message: intl.formatMessage(messages.logoutMessage),
confirm: intl.formatMessage(messages.logoutConfirm),
closeWhenConfirm: false,
onConfirm: () => logOut(),
}));
},
@ -51,7 +52,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>

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

Loading…
Cancel
Save