From 17735943d5cdde1eb852d36f1c3bb699d23f7eb6 Mon Sep 17 00:00:00 2001 From: shpuld Date: Mon, 14 Jan 2019 19:23:13 +0200 Subject: [PATCH 01/80] Add media viewer module and media module component, modify attachment behavior --- src/App.js | 2 + src/App.vue | 1 + src/components/attachment/attachment.js | 19 ++++++-- src/components/attachment/attachment.vue | 47 ++++++++++++-------- src/components/media_modal/media_modal.js | 51 ++++++++++++++++++++++ src/components/media_modal/media_modal.vue | 40 +++++++++++++++++ src/components/status/status.js | 10 ++++- src/components/status/status.vue | 9 +++- src/main.js | 4 +- src/modules/media_viewer.js | 40 +++++++++++++++++ 10 files changed, 197 insertions(+), 26 deletions(-) create mode 100644 src/components/media_modal/media_modal.js create mode 100644 src/components/media_modal/media_modal.vue create mode 100644 src/modules/media_viewer.js diff --git a/src/App.js b/src/App.js index 85df9416..83a61d39 100644 --- a/src/App.js +++ b/src/App.js @@ -6,6 +6,7 @@ import InstanceSpecificPanel from './components/instance_specific_panel/instance import FeaturesPanel from './components/features_panel/features_panel.vue' import WhoToFollowPanel from './components/who_to_follow_panel/who_to_follow_panel.vue' import ChatPanel from './components/chat_panel/chat_panel.vue' +import MediaModal from './components/media_modal/media_modal.vue' import SideDrawer from './components/side_drawer/side_drawer.vue' import { unseenNotificationsFromStore } from './services/notification_utils/notification_utils' @@ -20,6 +21,7 @@ export default { FeaturesPanel, WhoToFollowPanel, ChatPanel, + MediaModal, SideDrawer }, data: () => ({ diff --git a/src/App.vue b/src/App.vue index feadb009..833608ea 100644 --- a/src/App.vue +++ b/src/App.vue @@ -41,6 +41,7 @@ + diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 97c4f283..5e672ef2 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -7,7 +7,8 @@ const Attachment = { 'attachment', 'nsfw', 'statusId', - 'size' + 'size', + 'setMedia' ], data () { return { @@ -17,13 +18,17 @@ const Attachment = { loopVideo: this.$store.state.config.loopVideo, showHidden: false, loading: false, - img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img') + img: fileTypeService.fileType(this.attachment.mimetype) === 'image' && document.createElement('img'), + modalOpen: false } }, components: { StillImage }, computed: { + usePlaceHolder () { + return this.size === 'hide' || this.type === 'unknown' + }, type () { return fileTypeService.fileType(this.attachment.mimetype) }, @@ -37,7 +42,7 @@ const Attachment = { return this.size === 'small' }, fullwidth () { - return fileTypeService.fileType(this.attachment.mimetype) === 'html' + return this.type === 'html' || this.type === 'audio' } }, methods: { @@ -62,6 +67,14 @@ const Attachment = { this.showHidden = !this.showHidden } }, + toggleModal (event) { + if (this.type !== 'image' && this.type !== 'video') { + return + } + event.preventDefault() + this.setMedia() + this.$store.dispatch('setCurrent', this.attachment) + }, onVideoDataLoad (e) { if (typeof e.srcElement.webkitAudioDecodedByteCount !== 'undefined') { // non-zero if video has audio track diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 5eaa0d1d..1c6b84df 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -1,19 +1,29 @@ diff --git a/src/modules/media_viewer.js b/src/modules/media_viewer.js index 27714bae..a24b408d 100644 --- a/src/modules/media_viewer.js +++ b/src/modules/media_viewer.js @@ -28,7 +28,6 @@ const mediaViewer = { }, setCurrent ({ commit, state }, current) { const index = state.media.indexOf(current) - console.log(index, current) commit('setCurrent', index || 0) }, closeMediaViewer ({ commit }) { From 485a061287149fe0ff5c4a188d21d775ff271f6d Mon Sep 17 00:00:00 2001 From: shpuld Date: Sun, 20 Jan 2019 12:46:11 +0200 Subject: [PATCH 03/80] Polish for videos, smaller sizes, remove gif-looping options --- src/assets/nsfw.png | Bin 17071 -> 39603 bytes src/components/attachment/attachment.js | 39 +----------- src/components/attachment/attachment.vue | 67 ++++++++++++++------- src/components/media_modal/media_modal.js | 28 +-------- src/components/media_modal/media_modal.vue | 1 - src/components/settings/settings.js | 13 +--- src/components/settings/settings.vue | 9 --- static/font/config.json | 12 ++++ static/font/css/fontello-codes.css | 2 + static/font/css/fontello-embedded.css | 14 +++-- static/font/css/fontello-ie7-codes.css | 2 + static/font/css/fontello-ie7.css | 2 + static/font/css/fontello.css | 16 ++--- static/font/demo.html | 20 +++--- static/font/font/fontello.eot | Bin 16780 -> 17472 bytes static/font/font/fontello.svg | 6 +- static/font/font/fontello.ttf | Bin 16612 -> 17304 bytes static/font/font/fontello.woff | Bin 10228 -> 10572 bytes static/font/font/fontello.woff2 | Bin 8692 -> 8932 bytes 19 files changed, 104 insertions(+), 127 deletions(-) diff --git a/src/assets/nsfw.png b/src/assets/nsfw.png index 427490334ac6eafe0e69bea704685a5643a3c504..972bcb4caaf1e8a5f9e88ac46adb6ed8d4abfe2c 100644 GIT binary patch literal 39603 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

5KJrO2V;c|bq0+)wJ8skpK68-SKKCi&6* z)a?g2c3gO`YXg1d+pNE)ND)j3!j4p1_8YQbqD!1Xq(h3Un%~_Z`6zUo3zYtSFK4n+ z#C#A|s>?;*d4`vgzF|>W4>tO=ddKmx1wv|lVA(|DC;ab(EOK)bEJp%uA9oIF2)fMJ?GjaANc(me&p+hECP#56roHt))M zv&+g4nC@`nwuqs41TlZf10;w;4$12e!Q4Z61g*>AL`R#JzKJmjU%OQSBkg&6QQ6MHitPu~NxO@k^Y?kP_}B_;hy? zk%58~7-aatgP^)QJQ04-2m8z(Gm$a^2^e8G{W(i}-Y?69s&g%vs7u}af}>x%rbrEt z_mvqrOYT>Q-vn0b-dvf%tq0H{pjN1zK2obOKIlLgG$(D`=AOjV>rE=Qp#t3x}Fg~Qg0hlt_$+0al_N$iW(d-ZzZ4Y-s#W=zhTT{HAEdysx=P} z2OXx8C2tPu%L^JdXIVy`O8<})58wGQ_t{IZaV~56i{raE4lx_**C;JEkj>IAlbmKI zxa{P|(EFMwiHc@z@Zr1xG&H?2B=6AWfzW1mEgC7i$WzXQ0|J^<|Kn~@uR}=`1Fhx~ zYIH(?z5E89G>4xD4w2?$gyE|4{g13Op* zLi1+f_JW6Nh0uD+8^;N#6$=~wi6L2$L%d+|NB0<>)C_$13&Rp-yUk*o^FCMwx5f1M zSB^O^%-H2Ok0{(MAe$TbvJepkP^b<)RBC3Fe&Ox7&~k59bVzw0?AsaHp9-q>jy6Et zU6|mWe{X$$wqR6{-&Le&TyoicR@S{ANUTon;Q{x0{z7(QI+ zf9)3m(2`i40z)PAoBT0m+YNHdlX1>)>ocG;2MJ%_q+oH5XrZ11H0kINqJDF{G?BENw3ptT(^qKpCVfu_j%pgNg&Fg_sP{2K{E#Uxs})YMcm&W~a~T z`!J1-&*SUBemYpIM*>%g1B+yrdcn75c8zaO@WDZ=JSo^|-{uVS&weyv449CW?q-{G zj{|OEc>MD4cyphysihyP0_7sG6#vzfK|G0rI8l6eCVNLqG`25aRAJTUPRe^kt`REy zT5EKCA>4OscV4s=e^vwt5N)*`@wkqwk&?rmtit)5{M8wMzZa?Xwpl+K__sB)Ktg!RjRuU42LH6!`}Zi@R6A2CCwm_tv?YGi>%VO?+BP-6I*3w?1@8`>&Ij%R?Mu|m z`hcp_QLP*O+4mqQ;k4Xc&o$KSsl(idnJb_xLF2U+_L_q#6G@Z?bPLqt%_x!}{1EDk zlq*wrK-*nwa36OqO7bgp{lVqjU7wb|+z@RCvbD#b-50rPt~tI3`H$#}UNx$Ox^I`j zljRq9!f<(m(uGrxM(iL#D+6d3-ZG@`vw-0&_I5R@wroYlJxzK|X%QQ`j(_l$Rr%aA z$%F<@^GC8B^}e$DE$NjN95N!y%L^A>vpj1NpN>R2nk9;rbWmY{ME9j_h!^;7IM8q< z5`rul(U|P9fdRS5*RHWpqZ(J_dPVI|=bf*{kH4GtgV79F26qAVMU8_{7{gJ=lhKp- zlG#YQLn&7ZAPqeY-S(%>`+nZ&RkQ9Y0?xc4K$7kTsxkP!Wm0Um!T<)AcUCA7qO70dHoiOTztW_yCY_Zecy ze6Gu`siRzYcA04?y?v0iHj>r)N^CDuPY@pjtMBDL+kLzHailb0Ao0W_eq0S`nc4Rz znJ`1KO3MSuo+9;-p?ZkkoFkFe!wGQdwx_zAQI620PYuT zScbU@v7Ic)Ip$HjE*ZsTct&w&u>hg}QL^C=FMaHHMpgzg%mqk3LF>5k#9b zS7Y)dM5KCWz7erH69LelV^H$zh-(>8jdj8$PzGA4lILop{-VYUdcX5D>g?(44q0_G zX`q5~PfYK`DZ12-LadSz{c~IQ2|~e?_4b`Pzo~JJ!}%f5PC@;Xk_Lo3)@G?7R#ZhD z=(xzu7bMA7Z6iw1G_`p_Uoo!g-fuDpQS=@M9-FOLxoXMxq&z=ZcfbBHs8+;prLT*X z?>N=QB5hol(c70e_Q+;JCQJzCM){nOee~kvG@*W2V#N(wqOV_J&3c%53ac;Gh-jXb zl9<#JK*cPC^iCxjfVo2tqVzEtC&&~mICC9ohB63%mU*5dlgcaD z6~D|7eckXQmlkPJIn4cKGHVdB@IfTzSJqpbhXP{n5wcYahKzY#4H^h8*OI;?2O!m5 zqIgmA=M@!uD0s}UkoD1As-nM7-^If;(ZRoo8o8ye60{tf0b18rlo)!%$owfJULaz@0kA&lG!ff0GY@UVv_| zb9n!O>WG2$m$(jT_t~W!GMDY?R5)_UBqqla=ou6Ah)nUr$04r)X%DrnXON7CFE4ci z<$>+W#^q<&>u<)fbPc_Vx4$=-;Kk=yP4+mHy;nb)zW7z=veQwy6^gH*$k{HC0K`vE zLANOQUoK}>J=K4eSa=pwZoiTm(Bq!6>XUl}_%{nWHYWU8x_xc>d!=EPpN2;hZ>cmY z{jABv+e=lT0Yv67@+G|Xj6wh1J`ig)4jx9f+{t-q&Zbc_3V?^hpSWJOt(7Mk)1~{3 zbvI+J3K`P8>X&YFl8!VsTO6z>SK3)|iU^q#2JBsKes_dv>7d*OF#TyIU5y_ftJt6F z=7w@r<$wQ_;`7uM3y!y&GN>|5vY=XHV}2Fn@ajdw&*qj~psUI?#1W_P<0?3aWBjIO z;bZe(nyVwif9Umpq|M~y-d86LIj~&48z))0Vt^mhv@|dUk0ej*Ln zLqXKCp%z}9J@1j!sn7N2iNC@54-(aQwHwS_xTA}bY`KR+ymeaOkk4DBdLfFQNA8*0 z28Z7s`(HOpm3?+1VMZm1JUQ{*{&S^QANgt60mE&rfanojcX?rvv?cp8$KSCXS+A=q z;`sJ=RT@L4gO}zypKg~;6YmG{iG%*|Hr6sS>)O{Xithf5zcc)vgO3l9@++@VzruDJ zmA?vd-+-O*vW6&0p8S)jeEf!0`KzRPU12SHA1RCnH-<-P{cCfb6(_=d63ZDYFmHk? zXX%CVL(=QwJ%Kam?PswE>ey(hVAXwv{f*kWQ2{{c^a}MWyUz#!QP7@l61O!gv3+cS z(gJAYh2Hli}w(dvQueXl@6v0BM@;@ISdlyxS|JwBPpFq2<(W-K0j!rU7mJRq6 zFnW^29}+piT`rT2)R}AL^%>kAJqL*|(Bq#dsB67ZN4spMo_!amp~L{>(kL|HK)s|R>P!LLZ0!xS zSpOgHya(FSfieF7{r&;K@4z|(`1^lN^1$K$Y0dvPUpDZc?`HU4Uw!ay{}gJ|n}e5v S^w0k@PE}D$p<3>3*#8Bdc7`

- - + + + -
- Hide -
+ - + + + + @@ -44,13 +50,17 @@ diff --git a/src/components/media_modal/media_modal.js b/src/components/media_modal/media_modal.js index 7f10589c..14ae19d4 100644 --- a/src/components/media_modal/media_modal.js +++ b/src/components/media_modal/media_modal.js @@ -1,9 +1,11 @@ import StillImage from '../still-image/still-image.vue' +import VideoAttachment from '../video_attachment/video_attachment.vue' import fileTypeService from '../../services/file_type/file_type.service.js' const MediaModal = { components: { - StillImage + StillImage, + VideoAttachment }, computed: { showing () { @@ -17,9 +19,6 @@ const MediaModal = { }, type () { return this.currentMedia ? fileTypeService.fileType(this.currentMedia.mimetype) : null - }, - loopVideo () { - return this.$store.state.config.loopVideo } }, created () { diff --git a/src/components/media_modal/media_modal.vue b/src/components/media_modal/media_modal.vue index eb8fca53..796d4e40 100644 --- a/src/components/media_modal/media_modal.vue +++ b/src/components/media_modal/media_modal.vue @@ -1,14 +1,13 @@ @@ -18,7 +17,7 @@ @import '../../_variables.scss'; .modal-view { - z-index: 1005; + z-index: 1000; position: fixed; width: 100vw; height: 100vh; diff --git a/src/components/registration/registration.vue b/src/components/registration/registration.vue index 25c1a9d0..a3ba14d1 100644 --- a/src/components/registration/registration.vue +++ b/src/components/registration/registration.vue @@ -215,7 +215,7 @@ $validations-cRed: #f04124; } } -@media all and (max-width: 959px) { +@media all and (max-width: 800px) { .registration-form .container { flex-direction: column-reverse; } diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 76b42bab..06011e7c 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -13,6 +13,7 @@ const settings = { hideAttachmentsLocal: user.hideAttachments, hideAttachmentsInConvLocal: user.hideAttachmentsInConv, hideNsfwLocal: user.hideNsfw, + useOneClickNsfw: user.useOneClickNsfw, hideISPLocal: user.hideISP, preloadImage: user.preloadImage, @@ -56,7 +57,17 @@ const settings = { scopeCopyDefault: this.$t('settings.values.' + instance.scopeCopy), stopGifs: user.stopGifs, - webPushNotificationsLocal: user.webPushNotifications + webPushNotificationsLocal: user.webPushNotifications, + loopVideoSilentOnlyLocal: user.loopVideosSilentOnly, + loopSilentAvailable: + // Firefox + Object.getOwnPropertyDescriptor(HTMLVideoElement.prototype, 'mozHasAudio') || + // Chrome-likes + Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'webkitAudioDecodedByteCount') || + // Future spec, still not supported in Nightly 63 as of 08/2018 + Object.getOwnPropertyDescriptor(HTMLMediaElement.prototype, 'audioTracks'), + playVideosInline: user.playVideosInline, + useContainFit: user.useContainFit } }, components: { @@ -88,6 +99,9 @@ const settings = { hideNsfwLocal (value) { this.$store.dispatch('setOption', { name: 'hideNsfw', value }) }, + useOneClickNsfw (value) { + this.$store.dispatch('setOption', { name: 'useOneClickNsfw', value }) + }, preloadImage (value) { this.$store.dispatch('setOption', { name: 'preloadImage', value }) }, @@ -112,6 +126,9 @@ const settings = { loopVideoLocal (value) { this.$store.dispatch('setOption', { name: 'loopVideo', value }) }, + loopVideoSilentOnlyLocal (value) { + this.$store.dispatch('setOption', { name: 'loopVideoSilentOnly', value }) + }, autoLoadLocal (value) { this.$store.dispatch('setOption', { name: 'autoLoad', value }) }, @@ -146,6 +163,12 @@ const settings = { webPushNotificationsLocal (value) { this.$store.dispatch('setOption', { name: 'webPushNotifications', value }) if (value) this.$store.dispatch('registerPushNotifications') + }, + playVideosInline (value) { + this.$store.dispatch('setOption', { name: 'playVideosInline', value }) + }, + useContainFit (value) { + this.$store.dispatch('setOption', { name: 'useContainFit', value }) } } } diff --git a/src/components/settings/settings.vue b/src/components/settings/settings.vue index e84bd3f6..08d659d6 100644 --- a/src/components/settings/settings.vue +++ b/src/components/settings/settings.vue @@ -123,6 +123,10 @@ +
  • + + +
  • @@ -131,6 +135,23 @@
  • +
      +
    • + + +
      + ! {{$t('settings.limited_availability')}} +
      +
    • +
    +
  • +
  • + + +
  • +
  • + +
  • diff --git a/src/components/status/status.js b/src/components/status/status.js index 6e82307a..1db074e2 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -5,9 +5,11 @@ import DeleteButton from '../delete_button/delete_button.vue' import PostStatusForm from '../post_status_form/post_status_form.vue' import UserCardContent from '../user_card_content/user_card_content.vue' import StillImage from '../still-image/still-image.vue' +import Gallery from '../gallery/gallery.vue' import { filter, find } from 'lodash' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import fileType from 'src/services/file_type/file_type.service' const Status = { name: 'Status', @@ -197,6 +199,24 @@ const Status = { return 'small' } return 'normal' + }, + galleryTypes () { + if (this.attachmentSize === 'hide') { + return [] + } + return this.$store.state.config.playVideosInline + ? ['image'] + : ['image', 'video'] + }, + galleryAttachments () { + return this.status.attachments.filter( + file => fileType.fileMatchesSomeType(this.galleryTypes, file) + ) + }, + nonGalleryAttachments () { + return this.status.attachments.filter( + file => !fileType.fileMatchesSomeType(this.galleryTypes, file) + ) } }, components: { @@ -206,7 +226,8 @@ const Status = { DeleteButton, PostStatusForm, UserCardContent, - StillImage + StillImage, + Gallery }, methods: { visibilityIcon (visibility) { @@ -283,7 +304,7 @@ const Status = { return generateProfileLink(id, name, this.$store.state.instance.restrictedNicknames) }, setMedia () { - const attachments = this.status.attachments + const attachments = this.attachmentSize === 'hide' ? this.status.attachments : this.galleryAttachments return () => this.$store.dispatch('setMedia', attachments) } }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d7cab15b..ba3b9a4a 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -93,16 +93,23 @@ Show less -
    +
    - + :allowPlay="true" + :setMedia="setMedia()" + :key="attachment.id" + /> +
    @@ -568,7 +575,7 @@ a.unmute { } } -@media all and (max-width: 960px) { +@media all and (max-width: 800px) { .status-el { .retweet-info { .avatar { diff --git a/src/components/video_attachment/video_attachment.js b/src/components/video_attachment/video_attachment.js new file mode 100644 index 00000000..76b19a02 --- /dev/null +++ b/src/components/video_attachment/video_attachment.js @@ -0,0 +1,31 @@ + +const VideoAttachment = { + props: ['attachment', 'controls'], + data () { + return { + loopVideo: this.$store.state.config.loopVideo + } + }, + methods: { + onVideoDataLoad (e) { + const target = e.srcElement || e.target + if (typeof target.webkitAudioDecodedByteCount !== 'undefined') { + // non-zero if video has audio track + if (target.webkitAudioDecodedByteCount > 0) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } else if (typeof target.mozHasAudio !== 'undefined') { + // true if video has audio track + if (target.mozHasAudio) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } else if (typeof target.audioTracks !== 'undefined') { + if (target.audioTracks.length > 0) { + this.loopVideo = this.loopVideo && !this.$store.state.config.loopVideoSilentOnly + } + } + } + } +} + +export default VideoAttachment diff --git a/src/components/video_attachment/video_attachment.vue b/src/components/video_attachment/video_attachment.vue new file mode 100644 index 00000000..68de201e --- /dev/null +++ b/src/components/video_attachment/video_attachment.vue @@ -0,0 +1,11 @@ + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index 1dd3462b..6008287f 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -132,6 +132,7 @@ "hide_attachments_in_tl": "Hide attachments in timeline", "hide_isp": "Hide instance-specific panel", "preload_images": "Preload images", + "use_one_click_nsfw": "Open NSFW attachments with just one click", "hide_post_stats": "Hide post statistics (e.g. the number of favorites)", "hide_user_stats": "Hide user statistics (e.g. the number of followers)", "import_followers_from_a_csv_file": "Import follows from a csv file", @@ -148,6 +149,8 @@ "lock_account_description": "Restrict your account to approved followers only", "loop_video": "Loop videos", "loop_video_silent_only": "Loop only videos without sound (i.e. Mastodon's \"gifs\")", + "play_videos_inline": "Play videos directly on timeline", + "use_contain_fit": "Don't crop the attachment in thumbnails", "name": "Name", "name_bio": "Name & Bio", "new_password": "New password", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 08cfb617..969d5a9e 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -1,4 +1,16 @@ { + "chat": { + "title": "Chat" + }, + "features_panel": { + "chat": "Chat", + "gopher": "Gopher", + "media_proxy": "Media-välityspalvelin", + "scope_options": "Näkyvyyden rajaus", + "text_limit": "Tekstin pituusraja", + "title": "Ominaisuudet", + "who_to_follow": "Seurausehdotukset" + }, "finder": { "error_fetching_user": "Virhe hakiessa käyttäjää", "find_user": "Hae käyttäjä" @@ -9,85 +21,243 @@ }, "login": { "login": "Kirjaudu sisään", + "description": "Kirjaudu sisään OAuthilla", "logout": "Kirjaudu ulos", "password": "Salasana", - "placeholder": "esim. lain", + "placeholder": "esim. Seppo", "register": "Rekisteröidy", "username": "Käyttäjänimi" }, "nav": { + "about": "Tietoja", + "back": "Takaisin", + "chat": "Paikallinen Chat", + "friend_requests": "Seurauspyynnöt", "mentions": "Maininnat", + "dms": "Yksityisviestit", "public_tl": "Julkinen Aikajana", "timeline": "Aikajana", - "twkn": "Koko Tunnettu Verkosto" + "twkn": "Koko Tunnettu Verkosto", + "user_search": "Käyttäjähaku", + "who_to_follow": "Seurausehdotukset", + "preferences": "Asetukset" }, "notifications": { + "broken_favorite": "Viestiä ei löydetty...", "favorited_you": "tykkäsi viestistäsi", "followed_you": "seuraa sinua", + "load_older": "Lataa vanhempia ilmoituksia", "notifications": "Ilmoitukset", "read": "Lue!", "repeated_you": "toisti viestisi" }, "post_status": { + "new_status": "Uusi viesti", + "account_not_locked_warning": "Tilisi ei ole {0}. Kuka vain voi seurata sinua nähdäksesi 'vain-seuraajille' -viestisi", + "account_not_locked_warning_link": "lukittu", + "attachments_sensitive": "Merkkaa liitteet arkaluonteisiksi", + "content_type": { + "plain_text": "Tavallinen teksti" + }, + "content_warning": "Aihe (valinnainen)", "default": "Tulin juuri saunasta.", - "posting": "Lähetetään" + "direct_warning": "Tämä viesti näkyy vain mainituille käyttäjille.", + "posting": "Lähetetään", + "scope": { + "direct": "Yksityisviesti - Näkyy vain mainituille käyttäjille", + "private": "Vain-seuraajille - Näkyy vain seuraajillesi", + "public": "Julkinen - Näkyy julkisilla aikajanoilla", + "unlisted": "Listaamaton - Ei näy julkisilla aikajanoilla" + } }, "registration": { "bio": "Kuvaus", "email": "Sähköposti", "fullname": "Koko nimi", "password_confirm": "Salasanan vahvistaminen", - "registration": "Rekisteröityminen" + "registration": "Rekisteröityminen", + "token": "Kutsuvaltuus", + "captcha": "Varmenne", + "new_captcha": "Paina kuvaa saadaksesi uuden varmenteen", + "validations": { + "username_required": "ei voi olla tyhjä", + "fullname_required": "ei voi olla tyhjä", + "email_required": "ei voi olla tyhjä", + "password_required": "ei voi olla tyhjä", + "password_confirmation_required": "ei voi olla tyhjä", + "password_confirmation_match": "pitää vastata salasanaa" + } }, "settings": { + "attachmentRadius": "Liitteet", "attachments": "Liitteet", "autoload": "Lataa vanhempia viestejä automaattisesti ruudun pohjalla", "avatar": "Profiilikuva", + "avatarAltRadius": "Profiilikuvat (ilmoitukset)", + "avatarRadius": "Profiilikuvat", "background": "Tausta", "bio": "Kuvaus", + "btnRadius": "Napit", + "cBlue": "Sininen (Vastaukset, seuraukset)", + "cGreen": "Vihreä (Toistot)", + "cOrange": "Oranssi (Tykkäykset)", + "cRed": "Punainen (Peruminen)", + "change_password": "Vaihda salasana", + "change_password_error": "Virhe vaihtaessa salasanaa.", + "changed_password": "Salasana vaihdettu!", + "collapse_subject": "Minimoi viestit, joille on asetettu aihe", + "composing": "Viestien laatiminen", + "confirm_new_password": "Vahvista uusi salasana", "current_avatar": "Nykyinen profiilikuvasi", + "current_password": "Nykyinen salasana", "current_profile_banner": "Nykyinen julisteesi", + "data_import_export_tab": "Tietojen tuonti / vienti", + "default_vis": "Oletusnäkyvyysrajaus", + "delete_account": "Poista tili", + "delete_account_description": "Poista tilisi ja viestisi pysyvästi.", + "delete_account_error": "Virhe poistaessa tiliäsi. Jos virhe jatkuu, ota yhteyttä palvelimesi ylläpitoon.", + "delete_account_instructions": "Syötä salasanasi vahvistaaksesi tilin poiston.", + "export_theme": "Tallenna teema", "filtering": "Suodatus", "filtering_explanation": "Kaikki viestit, jotka sisältävät näitä sanoja, suodatetaan. Yksi sana per rivi.", + "follow_export": "Seurausten vienti", + "follow_export_button": "Vie seurauksesi CSV-tiedostoon", + "follow_export_processing": "Käsitellään, sinua pyydetään lataamaan tiedosto hetken päästä", + "follow_import": "Seurausten tuonti", + "follow_import_error": "Virhe tuodessa seuraksia", + "follows_imported": "Seuraukset tuotu! Niiden käsittely vie hetken.", "foreground": "Korostus", + "general": "Yleinen", "hide_attachments_in_convo": "Piilota liitteet keskusteluissa", "hide_attachments_in_tl": "Piilota liitteet aikajanalla", + "hide_isp": "Piilota palvelimenkohtainen ruutu", + "preload_images": "Esilataa kuvat", + "use_one_click_nsfw": "Avaa NSFW-liitteet yhdellä painalluksella", + "hide_post_stats": "Piilota viestien statistiikka (esim. tykkäysten määrä)", + "hide_user_stats": "Piilota käyttäjien statistiikka (esim. seuraajien määrä)", + "import_followers_from_a_csv_file": "Tuo seuraukset CSV-tiedostosta", + "import_theme": "Tuo tallennettu teema", + "inputRadius": "Syöttökentät", + "checkboxRadius": "Valintalaatikot", + "instance_default": "(oletus: {value})", + "instance_default_simple": "(oletus)", + "interface": "Käyttöliittymä", + "interfaceLanguage": "Käyttöliittymän kieli", + "invalid_theme_imported": "Tuotu tallennettu teema on epäkelpo, muutoksia ei tehty nykyiseen teemaasi.", + "limited_availability": "Ei saatavilla selaimessasi", "links": "Linkit", + "lock_account_description": "Vain erikseen hyväksytyt käyttäjät voivat seurata tiliäsi", + "loop_video": "Uudelleentoista videot", + "loop_video_silent_only": "Uudelleentoista ainoastaan äänettömät videot (Video-\"giffit\")", + "play_videos_inline": "Toista videot suoraan aikajanalla", + "use_contain_fit": "Älä rajaa liitteitä esikatselussa", "name": "Nimi", "name_bio": "Nimi ja kuvaus", - "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse.", + "new_password": "Uusi salasana", + "notification_visibility": "Ilmoitusten näkyvyys", + "notification_visibility_follows": "Seuraukset", + "notification_visibility_likes": "Tykkäykset", + "notification_visibility_mentions": "Maininnat", + "notification_visibility_repeats": "Toistot", + "no_rich_text_description": "Älä näytä tekstin muotoilua.", + "hide_network_description": "Älä näytä seurauksiani tai seuraajiani", + "nsfw_clickthrough": "Piilota NSFW liitteet klikkauksen taakse", + "panelRadius": "Ruudut", + "pause_on_unfocused": "Pysäytä automaattinen viestien näyttö välilehden ollessa pois fokuksesta", "presets": "Valmiit teemat", "profile_background": "Taustakuva", "profile_banner": "Juliste", + "profile_tab": "Profiili", + "radii_help": "Aseta reunojen pyöristys (pikseleinä)", + "replies_in_timeline": "Keskustelut aikajanalla", "reply_link_preview": "Keskusteluiden vastauslinkkien esikatselu", + "reply_visibility_all": "Näytä kaikki vastaukset", + "reply_visibility_following": "Näytä vain vastaukset minulle tai seuraamilleni käyttäjille", + "reply_visibility_self": "Näytä vain vastaukset minulle", + "saving_err": "Virhe tallentaessa asetuksia", + "saving_ok": "Asetukset tallennettu", + "security_tab": "Tietoturva", + "scope_copy": "Kopioi näkyvyysrajaus vastatessa (Yksityisviestit aina kopioivat)", "set_new_avatar": "Aseta uusi profiilikuva", "set_new_profile_background": "Aseta uusi taustakuva", "set_new_profile_banner": "Aseta uusi juliste", "settings": "Asetukset", + "subject_input_always_show": "Näytä aihe-kenttä", + "subject_line_behavior": "Aihe-kentän kopiointi", + "subject_line_email": "Kuten sähköposti: \"re: aihe\"", + "subject_line_mastodon": "Kopioi sellaisenaan", + "subject_line_noop": "Älä kopioi", + "stop_gifs": "Toista giffit vain kohdistaessa", "streaming": "Näytä uudet viestit automaattisesti ollessasi ruudun huipulla", "text": "Teksti", "theme": "Teema", "theme_help": "Käytä heksadesimaalivärejä muokataksesi väriteemaasi.", - "user_settings": "Käyttäjän asetukset" + "theme_help_v2_1": "Voit asettaa tiettyjen osien värin tai läpinäkyvyyden täyttämällä valintalaatikon, käytä \"Tyhjennä kaikki\"-nappia tyhjentääksesi kaiken.", + "theme_help_v2_2": "Ikonit kenttien alla ovat kontrasti-indikaattoreita, lisätietoa kohdistamalla. Käyttäessä läpinäkyvyyttä ne näyttävät pahimman skenaarion.", + "tooltipRadius": "Ohje- tai huomioviestit", + "user_settings": "Käyttäjän asetukset", + "values": { + "false": "pois päältä", + "true": "päällä" + } }, "timeline": { "collapse": "Sulje", "conversation": "Keskustelu", "error_fetching": "Virhe ladatessa viestejä", "load_older": "Lataa vanhempia viestejä", + "no_retweet_hint": "Viesti ei ole julkinen, eikä sitä voi toistaa", "repeated": "toisti", "show_new": "Näytä uudet", "up_to_date": "Ajantasalla" }, "user_card": { + "approve": "Hyväksy", + "block": "Estä", + "blocked": "Estetty!", + "deny": "Älä hyväksy", "follow": "Seuraa", + "follow_sent": "Pyyntö lähetetty!", + "follow_progress": "Pyydetään...", + "follow_again": "Lähetä pyyntö uudestaan", + "follow_unfollow": "Älä seuraa", "followees": "Seuraa", "followers": "Seuraajat", "following": "Seuraat!", "follows_you": "Seuraa sinua!", + "its_you": "Sinun tili!", "mute": "Hiljennä", "muted": "Hiljennetty", "per_day": "päivässä", + "remote_follow": "Seuraa muualta", "statuses": "Viestit" + }, + "user_profile": { + "timeline_title": "Käyttäjän aikajana" + }, + "who_to_follow": { + "more": "Lisää", + "who_to_follow": "Seurausehdotukset" + }, + "tool_tip": { + "media_upload": "Lataa tiedosto", + "repeat": "Toista", + "reply": "Vastaa", + "favorite": "Tykkää", + "user_settings": "Käyttäjäasetukset" + }, + "upload":{ + "error": { + "base": "Lataus epäonnistui.", + "file_too_big": "Tiedosto liian suuri [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]", + "default": "Yritä uudestaan myöhemmin" + }, + "file_size_units": { + "B": "tavua", + "KiB": "kt", + "MiB": "Mt", + "GiB": "Gt", + "TiB": "Tt" + } } } diff --git a/src/services/file_type/file_type.service.js b/src/services/file_type/file_type.service.js index f543ec79..2a046bec 100644 --- a/src/services/file_type/file_type.service.js +++ b/src/services/file_type/file_type.service.js @@ -1,27 +1,32 @@ -const fileType = (typeString) => { - let type = 'unknown' - - if (typeString.match(/text\/html/)) { - type = 'html' +// TODO this func might as well take the entire file and use its mimetype +// or the entire service could be just mimetype service that only operates +// on mimetypes and not files. Currently the naming is confusing. +const fileType = mimetype => { + if (mimetype.match(/text\/html/)) { + return 'html' } - if (typeString.match(/image/)) { - type = 'image' + if (mimetype.match(/image/)) { + return 'image' } - if (typeString.match(/video/)) { - type = 'video' + if (mimetype.match(/video/)) { + return 'video' } - if (typeString.match(/audio/)) { - type = 'audio' + if (mimetype.match(/audio/)) { + return 'audio' } - return type + return 'unknown' } +const fileMatchesSomeType = (types, file) => + types.some(type => fileType(file.mimetype) === type) + const fileTypeService = { - fileType + fileType, + fileMatchesSomeType } export default fileTypeService diff --git a/test/unit/specs/services/file_type/file_type.spec.js b/test/unit/specs/services/file_type/file_type.spec.js new file mode 100644 index 00000000..eb8a087d --- /dev/null +++ b/test/unit/specs/services/file_type/file_type.spec.js @@ -0,0 +1,19 @@ +import fileType from 'src/services/file_type/file_type.service.js' + +describe('fileType service', () => { + describe('fileMatchesSomeType', () => { + it('should be true when file type is one of the listed', () => { + const file = { mimetype: 'audio/mpeg' } + const types = ['video', 'audio'] + + expect(fileType.fileMatchesSomeType(types, file)).to.eql(true) + }) + + it('should be false when files type is not included in type list', () => { + const file = { mimetype: 'audio/mpeg' } + const types = ['image', 'video'] + + expect(fileType.fileMatchesSomeType(types, file)).to.eql(false) + }) + }) +}) From 676acdbf82726cd78ce8611d4c6ba78c58b8fc4c Mon Sep 17 00:00:00 2001 From: shpuld Date: Sat, 26 Jan 2019 17:59:59 +0200 Subject: [PATCH 05/80] Add default config for new options, fix firefox inline playing --- src/components/attachment/attachment.vue | 2 +- static/config.json | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 5a80db8a..7e972026 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -34,7 +34,7 @@ @click="openModal" v-if="type === 'video' && !hidden" :class="{'small': isSmall}" - :href="attachment.url" + :href="allowPlay ? undefined : attachment.url" > diff --git a/static/config.json b/static/config.json index 24e26696..aac93f70 100644 --- a/static/config.json +++ b/static/config.json @@ -19,5 +19,8 @@ "loginMethod": "password", "webPushNotifications": false, "noAttachmentLinks": false, - "nsfwCensorImage": "" + "nsfwCensorImage": "", + "useOneClickNsfw": true, + "playVideosInline": false, + "useContainFit": false } From 4c9f3b946db181b6069df9290516c33a1f71233d Mon Sep 17 00:00:00 2001 From: shpuld Date: Sun, 27 Jan 2019 08:42:34 +0200 Subject: [PATCH 06/80] Add forgotten prop to entity normalizer --- src/services/entity_normalizer/entity_normalizer.service.js | 1 + .../specs/services/entity_normalizer/entity_normalizer.spec.js | 1 + 2 files changed, 2 insertions(+) diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index becbab15..4e24d39e 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -104,6 +104,7 @@ export const parseUser = (data) => { output.background_image = data.background_image } + output.following = data.following output.created_at = new Date(data.created_at) output.locked = data.locked output.followers_count = data.followers_count diff --git a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js index 703fecf1..c1d5ac12 100644 --- a/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js +++ b/test/unit/specs/services/entity_normalizer/entity_normalizer.spec.js @@ -80,6 +80,7 @@ const makeMockUserMasto = (overrides = {}) => { fields: [], followers_count: 705, following_count: 326, + following: true, header: 'https://shigusegubu.club/media/7ab024d9-2a8a-4fbc-9ce8-da06756ae2db/6aadefe4e264133bc377ab450e6b045b6f5458542a5c59e6c741f86107f0388b.png', header_static: From 2d94a49798783d404ecb038f06fa877fd7885d2e Mon Sep 17 00:00:00 2001 From: shpuld Date: Sun, 27 Jan 2019 11:13:32 +0200 Subject: [PATCH 07/80] Fix registration path warning and translation warnings --- src/boot/routes.js | 2 +- src/components/user_profile/user_profile.vue | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/boot/routes.js b/src/boot/routes.js index 9dba532a..cfbcb1fe 100644 --- a/src/boot/routes.js +++ b/src/boot/routes.js @@ -39,7 +39,7 @@ export default (store) => { { name: 'dms', path: '/users/:username/dms', component: DMs }, { name: 'settings', path: '/settings', component: Settings }, { name: 'registration', path: '/registration', component: Registration }, - { name: 'registration', path: '/registration/:token', component: Registration }, + { name: 'registration-token', path: '/registration/:token', component: Registration }, { name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, { name: 'user-settings', path: '/user-settings', component: UserSettings }, { name: 'notifications', path: '/:username/notifications', component: Notifications }, diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index 74cd9c53..f9b964ce 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -20,8 +20,8 @@
    - - + + + +
    -
    +
    {{authError}} From 2ce6fba2ba968a99f395e4bddaf1f43492cce576 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 28 Jan 2019 17:52:01 +0100 Subject: [PATCH 20/80] Rename service worker to not clash with mastodon sw. --- build/webpack.base.conf.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index ea46ce6f..e07bb7a2 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -95,7 +95,8 @@ module.exports = { }, plugins: [ new ServiceWorkerWebpackPlugin({ - entry: path.join(__dirname, '..', 'src/sw.js') + entry: path.join(__dirname, '..', 'src/sw.js'), + filename: 'sw-pleroma.js' }) ] } From ca78623b348a3ef83c02e451b0b553b431a06551 Mon Sep 17 00:00:00 2001 From: shpuld Date: Mon, 28 Jan 2019 20:15:00 +0200 Subject: [PATCH 21/80] fix user tl clearing and give it a test --- src/modules/statuses.js | 6 +++--- test/unit/specs/modules/statuses.spec.js | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 3d6ea2f7..04ec4dbc 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -2,7 +2,7 @@ import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'l import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' -const emptyTl = () => ({ +const emptyTl = (userId = 0) => ({ statuses: [], statusesObject: {}, faves: [], @@ -14,7 +14,7 @@ const emptyTl = () => ({ loading: false, followers: [], friends: [], - userId: 0, + userId, flushMarker: 0 }) @@ -319,7 +319,7 @@ export const mutations = { each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status }) }, clearTimeline (state, { timeline }) { - state.timelines[timeline] = emptyTl() + state.timelines[timeline] = emptyTl(state.timelines[timeline].userId) }, setFavorited (state, { status, value }) { const newStatus = state.allStatusesObject[status.id] diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index 33628b9b..01d2ce06 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -240,6 +240,15 @@ describe('The Statuses module', () => { expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true) }) + it('keeps userId when clearing user timeline', () => { + const state = cloneDeep(defaultState) + state.timelines.user.userId = 123 + + mutations.clearTimeline(state, { timeline: 'user' }) + + expect(state.timelines.user.userId).to.eql(123) + }) + describe('notifications', () => { it('removes a notification when the notice gets removed', () => { const user = { id: '1' } From a593c2cb8cf77ea1d894a941c29858630e703a36 Mon Sep 17 00:00:00 2001 From: shpuld Date: Mon, 28 Jan 2019 21:21:11 +0200 Subject: [PATCH 22/80] Make userByName case-insensitive --- src/modules/users.js | 2 +- test/unit/specs/modules/users.spec.js | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/modules/users.js b/src/modules/users.js index d83f0dd8..181946b4 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -91,7 +91,7 @@ export const getters = { userById: state => id => state.users.find(user => user.id === id), userByName: state => name => - state.users.find(user => user.screen_name === name) + state.users.find(user => user.screen_name.toLowerCase() === name.toLowerCase()) } export const defaultState = { diff --git a/test/unit/specs/modules/users.spec.js b/test/unit/specs/modules/users.spec.js index b0f3c51e..4d49ee24 100644 --- a/test/unit/specs/modules/users.spec.js +++ b/test/unit/specs/modules/users.spec.js @@ -45,6 +45,17 @@ describe('The users module', () => { const expected = { screen_name: 'Guy', id: '1' } expect(getters.userByName(state)(name)).to.eql(expected) }) + + it('returns user with matching screen_name with different case', () => { + const state = { + users: [ + { screen_name: 'guy', id: '1' } + ] + } + const name = 'Guy' + const expected = { screen_name: 'guy', id: '1' } + expect(getters.userByName(state)(name)).to.eql(expected) + }) }) describe('getUserById', () => { From 0460728a8c571b9a4468c2f93a2f8465fe37fa86 Mon Sep 17 00:00:00 2001 From: shpuld Date: Mon, 28 Jan 2019 21:59:17 +0200 Subject: [PATCH 23/80] Fix tall status expanding needing extra clicks, make tall status expand automatically in conversation when focused --- src/components/status/status.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 13e79dd0..c93580c0 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -31,7 +31,7 @@ const Status = { userExpanded: false, preview: null, showPreview: false, - showingTall: false, + showingTall: this.inConversation && this.focused, expandingSubject: typeof this.$store.state.config.collapseMessageWithSubject === 'undefined' ? !this.$store.state.instance.collapseMessageWithSubject : !this.$store.state.config.collapseMessageWithSubject, @@ -262,13 +262,14 @@ const Status = { this.userExpanded = !this.userExpanded }, toggleShowMore () { + console.log('toggleShowMore', this.showingTall) if (this.showingTall) { this.showingTall = false - } else if (this.expandingSubject) { + } else if (this.expandingSubject && this.status.summary) { this.expandingSubject = false } else if (this.hideTallStatus) { this.showingTall = true - } else if (this.hideSubjectStatus) { + } else if (this.hideSubjectStatus && this.status.summary) { this.expandingSubject = true } }, @@ -301,8 +302,10 @@ const Status = { 'highlight': function (id) { if (this.status.id === id) { let rect = this.$el.getBoundingClientRect() - if (rect.top < 100) { + if (rect.top < 140) { window.scrollBy(0, rect.top - 200) + } else if (rect.top < window.innerHeight && rect.height >= (window.innerHeight - 50)) { + window.scrollBy(0, rect.top - 50) } else if (rect.bottom > window.innerHeight - 50) { window.scrollBy(0, rect.bottom - window.innerHeight + 50) } From c3a931610a16a23659edc1140573a003e14c8dbc Mon Sep 17 00:00:00 2001 From: shpuld Date: Mon, 28 Jan 2019 22:03:25 +0200 Subject: [PATCH 24/80] remove log --- src/components/status/status.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index c93580c0..443e6b42 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -262,7 +262,6 @@ const Status = { this.userExpanded = !this.userExpanded }, toggleShowMore () { - console.log('toggleShowMore', this.showingTall) if (this.showingTall) { this.showingTall = false } else if (this.expandingSubject && this.status.summary) { From 1f3cf6d1fc21df8b27210ec2b6a4700b1a493d61 Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 16:32:34 +0200 Subject: [PATCH 25/80] add missing shadow to nav panel --- src/components/nav_panel/nav_panel.vue | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index ad7c53f9..3aa0a793 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -44,6 +44,7 @@ .nav-panel .panel { overflow: hidden; + box-shadow: var(--panelShadow); } .nav-panel ul { list-style: none; From 41c4d59341468ad8e061bd0fd5fb3cf92ff23dfa Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 17:16:25 +0200 Subject: [PATCH 26/80] Connect only after-store instead of when user gets token --- src/boot/after_store.js | 2 ++ src/modules/api.js | 11 +++++++++-- src/modules/users.js | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/boot/after_store.js b/src/boot/after_store.js index 50500582..5693dcc6 100644 --- a/src/boot/after_store.js +++ b/src/boot/after_store.js @@ -89,6 +89,8 @@ const afterStoreSetup = ({ store, i18n }) => { if ((config.chatDisabled)) { store.dispatch('disableChat') + } else { + store.dispatch('initializeSocket') } const router = new VueRouter({ diff --git a/src/modules/api.js b/src/modules/api.js index a61340c2..7bda13e7 100644 --- a/src/modules/api.js +++ b/src/modules/api.js @@ -20,6 +20,9 @@ const api = { removeFetcher (state, {timeline}) { delete state.fetchers[timeline] }, + setWsToken (state, token) { + state.wsToken = token + }, setSocket (state, socket) { state.socket = socket }, @@ -51,10 +54,14 @@ const api = { window.clearInterval(fetcher) store.commit('removeFetcher', {timeline}) }, - initializeSocket (store, token) { + setWsToken (store, token) { + store.commit('setWsToken', token) + }, + initializeSocket (store) { // Set up websocket connection if (!store.state.chatDisabled) { - let socket = new Socket('/socket', {params: {token: token}}) + const token = store.state.wsToken + const socket = new Socket('/socket', {params: {token}}) socket.connect() store.dispatch('initializeChat', socket) } diff --git a/src/modules/users.js b/src/modules/users.js index 181946b4..2533912c 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -222,10 +222,10 @@ const users = { commit('setBackendInteractor', backendInteractorService(accessToken)) if (user.token) { - store.dispatch('initializeSocket', user.token) + store.dispatch('setWsToken', user.token) } - // Start getting fresh tweets. + // Start getting fresh posts. store.dispatch('startFetching', 'friends') // Get user mutes and follower info From b042c48fa72b16ae4a4f75a353627bbb5f5ddc72 Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 17:38:55 +0200 Subject: [PATCH 27/80] Fix TypeError crash when using url of an invalid user --- src/modules/users.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/users.js b/src/modules/users.js index 181946b4..dd555f6c 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -91,7 +91,9 @@ export const getters = { userById: state => id => state.users.find(user => user.id === id), userByName: state => name => - state.users.find(user => user.screen_name.toLowerCase() === name.toLowerCase()) + state.users.find(user => user.screen_name && + (user.screen_name.toLowerCase() === name.toLowerCase()) + ) } export const defaultState = { From 89bfc41174e1da8275e01c0280eec5ed7197b4d1 Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 18:40:49 +0200 Subject: [PATCH 28/80] Add a better check for fresh TL flushing --- src/services/timeline_fetcher/timeline_fetcher.service.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 727f6c60..9c4a4820 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -29,9 +29,11 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false args['userId'] = userId args['tag'] = tag + const numStatusesBeforeFetch = timelineData.statuses.length + return apiService.fetchTimeline(args) .then((statuses) => { - if (!older && statuses.length >= 20 && !timelineData.loading && timelineData.statuses.length) { + if (!older && statuses.length >= 20 && !timelineData.loading && numStatusesBeforeFetch > 0) { store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) } update({store, statuses, timeline, showImmediately, userId}) From 0a0dffa793f2cea050507f811d1315348056348a Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 19:12:47 +0200 Subject: [PATCH 29/80] Fix favs timeline fetching when opening profile via direct url --- src/components/user_profile/user_profile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js index 991062cd..27e138b0 100644 --- a/src/components/user_profile/user_profile.js +++ b/src/components/user_profile/user_profile.js @@ -36,7 +36,8 @@ const UserProfile = { return this.$route.params.name || this.user.screen_name }, isUs () { - return this.userId === this.$store.state.users.currentUser.id + return this.userId && this.$store.state.users.currentUser.id && + this.userId === this.$store.state.users.currentUser.id }, friends () { return this.user.friends From 62e9525724cfff0c3e26b8325b019e926baed1ca Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 21:04:52 +0200 Subject: [PATCH 30/80] Add loading indicator for notifications, make timelines indicate bottoming out when no more statuses --- src/components/notifications/notifications.js | 14 ++++++++++++++ src/components/notifications/notifications.vue | 9 +++++++-- src/components/timeline/timeline.js | 10 ++++++++-- src/components/timeline/timeline.vue | 9 +++++++-- src/i18n/en.json | 6 ++++-- src/i18n/fi.json | 6 ++++-- src/modules/statuses.js | 7 +++++++ .../notifications_fetcher.service.js | 1 + .../timeline_fetcher/timeline_fetcher.service.js | 1 + 9 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/components/notifications/notifications.js b/src/components/notifications/notifications.js index ea32bbd0..5e95631a 100644 --- a/src/components/notifications/notifications.js +++ b/src/components/notifications/notifications.js @@ -13,6 +13,11 @@ const Notifications = { notificationsFetcher.startFetching({ store, credentials }) }, + data () { + return { + bottomedOut: false + } + }, computed: { notifications () { return notificationsFromStore(this.$store) @@ -28,6 +33,9 @@ const Notifications = { }, unseenCount () { return this.unseenNotifications.length + }, + loading () { + return this.$store.state.statuses.notifications.loading } }, components: { @@ -49,10 +57,16 @@ const Notifications = { fetchOlderNotifications () { const store = this.$store const credentials = store.state.users.currentUser.credentials + store.commit('setNotificationsLoading', { value: true }) notificationsFetcher.fetchAndUpdate({ store, credentials, older: true + }).then(notifs => { + store.commit('setNotificationsLoading', { value: false }) + if (notifs.length === 0) { + this.bottomedOut = true + } }) } } diff --git a/src/components/notifications/notifications.vue b/src/components/notifications/notifications.vue index 64f18720..6f162b62 100644 --- a/src/components/notifications/notifications.vue +++ b/src/components/notifications/notifications.vue @@ -18,10 +18,15 @@
    diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 98da8660..85e0a055 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -16,7 +16,8 @@ const Timeline = { data () { return { paused: false, - unfocused: false + unfocused: false, + bottomedOut: false } }, computed: { @@ -95,7 +96,12 @@ const Timeline = { showImmediately: true, userId: this.userId, tag: this.tag - }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false })) + }).then(statuses => { + store.commit('setLoading', { timeline: this.timelineName, value: false }) + if (statuses.length === 0) { + this.bottomedOut = true + } + }) }, 1000, this), scrollLoad (e) { const bodyBRect = document.body.getBoundingClientRect() diff --git a/src/components/timeline/timeline.vue b/src/components/timeline/timeline.vue index 6ba598c5..e3eea3bd 100644 --- a/src/components/timeline/timeline.vue +++ b/src/components/timeline/timeline.vue @@ -20,10 +20,15 @@ diff --git a/src/i18n/en.json b/src/i18n/en.json index 3ff98ab0..c2f48450 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -49,7 +49,8 @@ "load_older": "Load older notifications", "notifications": "Notifications", "read": "Read!", - "repeated_you": "repeated your status" + "repeated_you": "repeated your status", + "no_more_notifications": "No more notifications" }, "post_status": { "new_status": "Post new status", @@ -317,7 +318,8 @@ "no_retweet_hint": "Post is marked as followers-only or direct and cannot be repeated", "repeated": "repeated", "show_new": "Show new", - "up_to_date": "Up-to-date" + "up_to_date": "Up-to-date", + "no_more_statuses": "No more statuses" }, "user_card": { "approve": "Approve", diff --git a/src/i18n/fi.json b/src/i18n/fi.json index 08cfb617..ee7cd4d2 100644 --- a/src/i18n/fi.json +++ b/src/i18n/fi.json @@ -26,7 +26,8 @@ "followed_you": "seuraa sinua", "notifications": "Ilmoitukset", "read": "Lue!", - "repeated_you": "toisti viestisi" + "repeated_you": "toisti viestisi", + "no_more_notifications": "Ei enempää ilmoituksia" }, "post_status": { "default": "Tulin juuri saunasta.", @@ -77,7 +78,8 @@ "load_older": "Lataa vanhempia viestejä", "repeated": "toisti", "show_new": "Näytä uudet", - "up_to_date": "Ajantasalla" + "up_to_date": "Ajantasalla", + "no_more_statuses": "Ei enempää viestejä" }, "user_card": { "follow": "Seuraa", diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 04ec4dbc..56619455 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -28,6 +28,7 @@ export const defaultState = { minId: Number.POSITIVE_INFINITY, data: [], idStore: {}, + loading: false, error: false }, favorites: new Set(), @@ -348,6 +349,9 @@ export const mutations = { setError (state, { value }) { state.error = value }, + setNotificationsLoading (state, { value }) { + state.notifications.loading = value + }, setNotificationsError (state, { value }) { state.notifications.error = value }, @@ -376,6 +380,9 @@ const statuses = { setError ({ rootState, commit }, { value }) { commit('setError', { value }) }, + setNotificationsLoading ({ rootState, commit }, { value }) { + commit('setNotificationsLoading', { value }) + }, setNotificationsError ({ rootState, commit }, { value }) { commit('setNotificationsError', { value }) }, diff --git a/src/services/notifications_fetcher/notifications_fetcher.service.js b/src/services/notifications_fetcher/notifications_fetcher.service.js index 1480cded..b69ec643 100644 --- a/src/services/notifications_fetcher/notifications_fetcher.service.js +++ b/src/services/notifications_fetcher/notifications_fetcher.service.js @@ -24,6 +24,7 @@ const fetchAndUpdate = ({store, credentials, older = false}) => { return apiService.fetchTimeline(args) .then((notifications) => { update({store, notifications, older}) + return notifications }, () => store.dispatch('setNotificationsError', { value: true })) .catch(() => store.dispatch('setNotificationsError', { value: true })) } diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js index 727f6c60..08d91b5a 100644 --- a/src/services/timeline_fetcher/timeline_fetcher.service.js +++ b/src/services/timeline_fetcher/timeline_fetcher.service.js @@ -35,6 +35,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId }) } update({store, statuses, timeline, showImmediately, userId}) + return statuses }, () => store.dispatch('setError', { value: true })) } From 7666d50d8a9eb2ef8b5a61a1689b7358ae7d93ea Mon Sep 17 00:00:00 2001 From: shpuld Date: Tue, 29 Jan 2019 21:11:00 +0200 Subject: [PATCH 31/80] Change panel-footing style to be more fitting --- src/App.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/App.scss b/src/App.scss index ba8770e2..f3e9e557 100644 --- a/src/App.scss +++ b/src/App.scss @@ -425,6 +425,12 @@ main-router { border-radius: 0 0 $fallback--panelRadius $fallback--panelRadius; border-radius: 0 0 var(--panelRadius, $fallback--panelRadius) var(--panelRadius, $fallback--panelRadius); + + .faint { + color: $fallback--faint; + color: var(--panelFaint, $fallback--faint); + } + a { color: $fallback--link; color: var(--panelLink, $fallback--link) From 298cea8f6d4b906ae38d761a086a53832300ea9e Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Wed, 30 Jan 2019 01:11:40 +0300 Subject: [PATCH 32/80] Split hide_network into hide_followers & hide_followings --- src/components/user_settings/user_settings.js | 9 ++++++--- src/components/user_settings/user_settings.vue | 8 ++++++-- src/i18n/de.json | 3 ++- src/i18n/en.json | 3 ++- src/i18n/ja.json | 3 ++- src/i18n/ko.json | 3 ++- src/i18n/ru.json | 3 ++- src/services/api/api.service.js | 2 +- .../entity_normalizer/entity_normalizer.service.js | 3 ++- 9 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/components/user_settings/user_settings.js b/src/components/user_settings/user_settings.js index dcce275a..be799f5d 100644 --- a/src/components/user_settings/user_settings.js +++ b/src/components/user_settings/user_settings.js @@ -10,7 +10,8 @@ const UserSettings = { newLocked: this.$store.state.users.currentUser.locked, newNoRichText: this.$store.state.users.currentUser.no_rich_text, newDefaultScope: this.$store.state.users.currentUser.default_scope, - newHideNetwork: this.$store.state.users.currentUser.hide_network, + hideFollowings: this.$store.state.users.currentUser.hide_followings, + hideFollowers: this.$store.state.users.currentUser.hide_followers, followList: null, followImportError: false, followsImported: false, @@ -66,7 +67,8 @@ const UserSettings = { /* eslint-disable camelcase */ const default_scope = this.newDefaultScope const no_rich_text = this.newNoRichText - const hide_network = this.newHideNetwork + const hide_followings = this.hideFollowings + const hide_followers = this.hideFollowers /* eslint-enable camelcase */ this.$store.state.api.backendInteractor .updateProfile({ @@ -78,7 +80,8 @@ const UserSettings = { /* eslint-disable camelcase */ default_scope, no_rich_text, - hide_network + hide_followings, + hide_followers /* eslint-enable camelcase */ }}).then((user) => { if (!user.error) { diff --git a/src/components/user_settings/user_settings.vue b/src/components/user_settings/user_settings.vue index 4bc2eeec..253bbd34 100644 --- a/src/components/user_settings/user_settings.vue +++ b/src/components/user_settings/user_settings.vue @@ -30,8 +30,12 @@

    - - + + +

    +

    + +

    diff --git a/src/i18n/de.json b/src/i18n/de.json index c87371e6..82860e9e 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -155,7 +155,8 @@ "notification_visibility_mentions": "Erwähnungen", "notification_visibility_repeats": "Wiederholungen", "no_rich_text_description": "Rich-Text Formatierungen von allen Beiträgen entfernen", - "hide_network_description": "Zeige nicht, wem ich folge und wer mir folgt", + "hide_followings_description": "Zeige nicht, wem ich folge", + "hide_followers_description": "Zeige nicht, wer mir folgt", "nsfw_clickthrough": "Aktiviere ausblendbares Overlay für Anhänge, die als NSFW markiert sind", "panelRadius": "Panel", "pause_on_unfocused": "Streaming pausieren, wenn das Tab nicht fokussiert ist", diff --git a/src/i18n/en.json b/src/i18n/en.json index 3ff98ab0..a2106f1a 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -157,7 +157,8 @@ "notification_visibility_mentions": "Mentions", "notification_visibility_repeats": "Repeats", "no_rich_text_description": "Strip rich text formatting from all posts", - "hide_network_description": "Don't show who I'm following and who's following me", + "hide_followings_description": "Don't show who I'm following", + "hide_followers_description": "Don't show who's following me", "nsfw_clickthrough": "Enable clickthrough NSFW attachment hiding", "panelRadius": "Panels", "pause_on_unfocused": "Pause streaming when tab is not focused", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 161856f0..37e96f6c 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -157,7 +157,8 @@ "notification_visibility_mentions": "メンション", "notification_visibility_repeats": "リピート", "no_rich_text_description": "リッチテキストをつかわない", - "hide_network_description": "わたしがフォローしているひとと、わたしをフォローしているひとを、みせない", + "hide_followings_description": "フォローしている人を表示しない", + "hide_followers_description": "フォローしている人を表示しない", "nsfw_clickthrough": "NSFWなファイルをかくす", "panelRadius": "パネル", "pause_on_unfocused": "タブにフォーカスがないときストリーミングをとめる", diff --git a/src/i18n/ko.json b/src/i18n/ko.json index 4b69df07..9f40be51 100644 --- a/src/i18n/ko.json +++ b/src/i18n/ko.json @@ -156,7 +156,8 @@ "notification_visibility_mentions": "멘션", "notification_visibility_repeats": "반복", "no_rich_text_description": "모든 게시물의 서식을 지우기", - "hide_network_description": "내 팔로우와 팔로워를 숨기기", + "hide_followings_description": "내가 팔로우하는 사람을 표시하지 않음", + "hide_followers_description": "나를 따르는 사람을 보여주지 마라.", "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화", "panelRadius": "패널", "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기", diff --git a/src/i18n/ru.json b/src/i18n/ru.json index 0887bb59..bf1e319f 100644 --- a/src/i18n/ru.json +++ b/src/i18n/ru.json @@ -127,7 +127,8 @@ "notification_visibility_mentions": "Упоминания", "notification_visibility_repeats": "Повторы", "no_rich_text_description": "Убрать форматирование из всех постов", - "hide_network_description": "Не показывать кого я читаю и кто меня читает", + "hide_followings_description": "Не показывать кого я читаю", + "hide_followers_description": "Не показывать кто читает меня", "nsfw_clickthrough": "Включить скрытие NSFW вложений", "panelRadius": "Панели", "pause_on_unfocused": "Приостановить загрузку когда вкладка не в фокусе", diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index 5b0d8650..31b48cb6 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -130,7 +130,7 @@ const updateBanner = ({credentials, params}) => { // description const updateProfile = ({credentials, params}) => { // Always include these fields, because they might be empty or false - const fields = ['description', 'locked', 'no_rich_text', 'hide_network'] + const fields = ['description', 'locked', 'no_rich_text', 'hide_followings', 'hide_followers'] let url = PROFILE_UPDATE_URL const form = new FormData() diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js index deffa537..fa955ad6 100644 --- a/src/services/entity_normalizer/entity_normalizer.service.js +++ b/src/services/entity_normalizer/entity_normalizer.service.js @@ -100,7 +100,8 @@ export const parseUser = (data) => { output.rights = data.rights output.no_rich_text = data.no_rich_text output.default_scope = data.default_scope - output.hide_network = data.hide_network + output.hide_followings = data.hide_followings + output.hide_followers = data.hide_followers output.background_image = data.background_image // on mastoapi this info is contained in a "relationship" output.following = data.following From 0a39159fdf99143ddf05580f066508e6d5a90773 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 16:38:28 +0200 Subject: [PATCH 33/80] Adjust scrolling logic and document it, make sure to never show 'show less' if it's not a tall status --- src/components/status/status.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 558125df..6ef13010 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -179,7 +179,7 @@ const Status = { return this.tallStatus }, showingMore () { - return this.showingTall || (this.status.summary && this.expandingSubject) + return (this.tallStatus && this.showingTall) || (this.status.summary && this.expandingSubject) }, nsfwClickthrough () { if (!this.status.nsfw) { @@ -303,11 +303,14 @@ const Status = { 'highlight': function (id) { if (this.status.id === id) { let rect = this.$el.getBoundingClientRect() - if (rect.top < 140) { - window.scrollBy(0, rect.top - 200) - } else if (rect.top < window.innerHeight && rect.height >= (window.innerHeight - 50)) { - window.scrollBy(0, rect.top - 50) + if (rect.top < 100) { + // Post is above screen, match its top to screen top + window.scrollBy(0, rect.top - 100) + } else if (rect.height >= (window.innerHeight - 50)) { + // Post we want to see is taller than screen so match its top to screen top + window.scrollBy(0, rect.top - 100) } else if (rect.bottom > window.innerHeight - 50) { + // Post is below screen, match its bottom to screen bottom window.scrollBy(0, rect.bottom - window.innerHeight + 50) } } From 16a5272726ec555d49276e291caa1d0ba71200e4 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 16:57:19 +0200 Subject: [PATCH 34/80] Fix a simple typo --- src/components/status/status.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/status/status.js b/src/components/status/status.js index 558125df..f85b7847 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -79,7 +79,7 @@ const Status = { }, replyProfileLink () { if (this.isReply) { - return this.generateUserProfileLink(this.status.in_reply_to_status_id, this.replyToName) + return this.generateUserProfileLink(this.status.in_reply_to_user_id, this.replyToName) } }, retweet () { return !!this.statusoid.retweeted_status }, From 15603981f8309d979465c40175f9b3cd4f6617b4 Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 19:15:35 +0200 Subject: [PATCH 35/80] Capture clicks on statuses to hijack mention clicks, match mention href to user somehow --- src/components/status/status.js | 15 ++++- src/components/status/status.vue | 4 +- .../mention_matcher/mention_matcher.js | 9 +++ .../mention_matcher/mention_matcher.spec.js | 63 +++++++++++++++++++ 4 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 src/services/mention_matcher/mention_matcher.js create mode 100644 test/unit/specs/services/mention_matcher/mention_matcher.spec.js diff --git a/src/components/status/status.js b/src/components/status/status.js index 558125df..e268ddaa 100644 --- a/src/components/status/status.js +++ b/src/components/status/status.js @@ -9,6 +9,7 @@ import LinkPreview from '../link-preview/link-preview.vue' import { filter, find } from 'lodash' import { highlightClass, highlightStyle } from '../../services/user_highlighter/user_highlighter.js' import generateProfileLink from 'src/services/user_profile_link_generator/user_profile_link_generator' +import { mentionMatchesUrl } from 'src/services/mention_matcher/mention_matcher.js' const Status = { name: 'Status', @@ -237,11 +238,23 @@ const Status = { return 'icon-globe' } }, - linkClicked ({target}) { + linkClicked (event) { + let { target } = event if (target.tagName === 'SPAN') { target = target.parentNode } if (target.tagName === 'A') { + if (target.className.match(/mention/)) { + const href = target.getAttribute('href') + const attn = this.status.attentions.find(attn => mentionMatchesUrl(attn, href)) + if (attn) { + event.stopPropagation() + event.preventDefault() + const link = this.generateUserProfileLink(attn.id, attn.screen_name) + this.$router.push(link) + return + } + } window.open(target.href, '_blank') } }, diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d88428c7..45100a46 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -24,9 +24,9 @@
    - + - +
    diff --git a/src/services/mention_matcher/mention_matcher.js b/src/services/mention_matcher/mention_matcher.js new file mode 100644 index 00000000..2c1ed970 --- /dev/null +++ b/src/services/mention_matcher/mention_matcher.js @@ -0,0 +1,9 @@ + +export const mentionMatchesUrl = (attention, url) => { + if (url === attention.statusnet_profile_url) { + return true + } + const [namepart, instancepart] = attention.screen_name.split('@') + const matchstring = new RegExp('://' + instancepart + '/.*' + namepart + '$', 'g') + return !!url.match(matchstring) +} diff --git a/test/unit/specs/services/mention_matcher/mention_matcher.spec.js b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js new file mode 100644 index 00000000..4f6f58ff --- /dev/null +++ b/test/unit/specs/services/mention_matcher/mention_matcher.spec.js @@ -0,0 +1,63 @@ +import * as MentionMatcher from 'src/services/mention_matcher/mention_matcher.js' + +const localAttn = () => ({ + id: 123, + is_local: true, + name: 'Guy', + screen_name: 'person', + statusnet_profile_url: 'https://instance.com/users/person' +}) + +const externalAttn = () => ({ + id: 123, + is_local: false, + name: 'Guy', + screen_name: 'person@instance.com', + statusnet_profile_url: 'https://instance.com/users/person' +}) + +describe('MentionMatcher', () => { + describe.only('mentionMatchesUrl', () => { + it('should match local mention', () => { + const attention = localAttn() + const url = 'https://instance.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match a local mention with same name but different instance', () => { + const attention = localAttn() + const url = 'https://website.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + + it('should match external pleroma mention', () => { + const attention = externalAttn() + const url = 'https://instance.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match external pleroma mention with same name but different instance', () => { + const attention = externalAttn() + const url = 'https://website.com/users/person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + + it('should match external mastodon mention', () => { + const attention = externalAttn() + const url = 'https://instance.com/@person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(true) + }) + + it('should not match external mastodon mention with same name but different instance', () => { + const attention = externalAttn() + const url = 'https://website.com/@person' + + expect(MentionMatcher.mentionMatchesUrl(attention, url)).to.eql(false) + }) + }) +}) From aaa5f1a6383d07b9fa80625a0dc4d16cb654cd5a Mon Sep 17 00:00:00 2001 From: shpuld Date: Wed, 30 Jan 2019 21:41:07 +0200 Subject: [PATCH 36/80] Adjust margins for attachments/previews --- src/components/gallery/gallery.vue | 3 +++ src/components/link-preview/link-preview.vue | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/gallery/gallery.vue b/src/components/gallery/gallery.vue index 20e8ab2f..3f90caa9 100644 --- a/src/components/gallery/gallery.vue +++ b/src/components/gallery/gallery.vue @@ -27,12 +27,15 @@ align-content: stretch; flex-grow: 1; margin-top: 0.5em; + margin-bottom: 0.25em; .attachments, .attachment { margin: 0 0.5em 0 0; flex-grow: 1; height: 100%; box-sizing: border-box; + // to make failed images a bit more noticeable on chromium + min-width: 2em; } .image-attachment { diff --git a/src/components/link-preview/link-preview.vue b/src/components/link-preview/link-preview.vue index 9b3f2550..e4a247c5 100644 --- a/src/components/link-preview/link-preview.vue +++ b/src/components/link-preview/link-preview.vue @@ -26,7 +26,7 @@ // TODO: clean up the random margins in attachments, this makes preview line // up with attachments... - margin-right: 0.7em; + margin-right: 0.5em; .card-image { flex-shrink: 0; From 3c4cf5ed1169bba6d5d2e4be5769c34d7c06da48 Mon Sep 17 00:00:00 2001 From: shadowfacts Date: Thu, 31 Jan 2019 00:50:19 +0000 Subject: [PATCH 37/80] Only generate mentions string if there are mentions Otherwise, a user replying to themselves without mentioning anyone else will insert a space at the beginning of the reply text area, hiding the placeholder. --- src/components/post_status_form/post_status_form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/post_status_form/post_status_form.js b/src/components/post_status_form/post_status_form.js index 8a4e2489..88bc736f 100644 --- a/src/components/post_status_form/post_status_form.js +++ b/src/components/post_status_form/post_status_form.js @@ -16,7 +16,7 @@ const buildMentionsString = ({user, attentions}, currentUser) => { return `@${attention.screen_name}` }) - return mentions.join(' ') + ' ' + return mentions.length > 0 ? mentions.join(' ') + ' ' : '' } const PostStatusForm = { From 6a8d10f0bb37aa71eabd4aa8849cf2d8cada2ae1 Mon Sep 17 00:00:00 2001 From: Edijs Date: Wed, 30 Jan 2019 18:53:34 -0700 Subject: [PATCH 38/80] Fix statistics label visibility via configuration --- src/components/user_card_content/user_card_content.vue | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue index 0e820182..d1034d68 100644 --- a/src/components/user_card_content/user_card_content.vue +++ b/src/components/user_card_content/user_card_content.vue @@ -107,18 +107,18 @@
    -
    +
    {{ $t('user_card.statuses') }}
    - {{user.statuses_count}}
    + {{user.statuses_count}}
    {{ $t('user_card.followees') }}
    - {{user.friends_count}} + {{user.friends_count}}
    {{ $t('user_card.followers') }}
    - {{user.followers_count}} + {{user.followers_count}}

    From 476bf0afe1c890b0f08587b3942981c8c591c7be Mon Sep 17 00:00:00 2001 From: shpuld Date: Thu, 31 Jan 2019 17:00:31 +0200 Subject: [PATCH 39/80] Fix media timeline attachments being small by adding an option to not render background tabs in tab switcher --- src/components/settings/settings.js | 2 +- .../style_switcher/style_switcher.js | 2 +- .../{tab_switcher.jsx => tab_switcher.js} | 12 +++-- src/components/user_profile/user_profile.vue | 48 ++++++++++++++++--- src/components/user_settings/user_settings.js | 2 +- .../user_settings/user_settings.vue | 2 +- 6 files changed, 52 insertions(+), 16 deletions(-) rename src/components/tab_switcher/{tab_switcher.jsx => tab_switcher.js} (85%) diff --git a/src/components/settings/settings.js b/src/components/settings/settings.js index 06011e7c..7000fa5e 100644 --- a/src/components/settings/settings.js +++ b/src/components/settings/settings.js @@ -1,5 +1,5 @@ /* eslint-env browser */ -import TabSwitcher from '../tab_switcher/tab_switcher.jsx' +import TabSwitcher from '../tab_switcher/tab_switcher.js' import StyleSwitcher from '../style_switcher/style_switcher.vue' import InterfaceLanguageSwitcher from '../interface_language_switcher/interface_language_switcher.vue' import { filter, trim } from 'lodash' diff --git a/src/components/style_switcher/style_switcher.js b/src/components/style_switcher/style_switcher.js index 6a4e1cba..8c3d4861 100644 --- a/src/components/style_switcher/style_switcher.js +++ b/src/components/style_switcher/style_switcher.js @@ -7,7 +7,7 @@ import OpacityInput from '../opacity_input/opacity_input.vue' import ShadowControl from '../shadow_control/shadow_control.vue' import FontControl from '../font_control/font_control.vue' import ContrastRatio from '../contrast_ratio/contrast_ratio.vue' -import TabSwitcher from '../tab_switcher/tab_switcher.jsx' +import TabSwitcher from '../tab_switcher/tab_switcher.js' import Preview from './preview.vue' import ExportImport from '../export_import/export_import.vue' diff --git a/src/components/tab_switcher/tab_switcher.jsx b/src/components/tab_switcher/tab_switcher.js similarity index 85% rename from src/components/tab_switcher/tab_switcher.jsx rename to src/components/tab_switcher/tab_switcher.js index 9038733c..f9c3f927 100644 --- a/src/components/tab_switcher/tab_switcher.jsx +++ b/src/components/tab_switcher/tab_switcher.js @@ -4,6 +4,7 @@ import './tab_switcher.scss' export default Vue.component('tab-switcher', { name: 'TabSwitcher', + props: ['renderOnlyFocused'], data () { return { active: this.$slots.default.findIndex(_ => _.tag) @@ -44,11 +45,12 @@ export default Vue.component('tab-switcher', { const contents = this.$slots.default.map((slot, index) => { if (!slot.tag) return const active = index === this.active - return ( -
    - {slot} -
    - ) + if (this.renderOnlyFocused) { + return active + ?
    {slot}
    + : + } + return
    {slot}
    }) return ( diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue index f9b964ce..e53ce4cc 100644 --- a/src/components/user_profile/user_profile.vue +++ b/src/components/user_profile/user_profile.vue @@ -1,12 +1,28 @@