aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/components/conversation/conversation.js9
-rw-r--r--src/components/status/status.js3
-rw-r--r--src/components/timeline/timeline.js17
-rw-r--r--src/components/user_card_content/user_card_content.vue16
-rw-r--r--src/components/user_profile/user_profile.js12
-rw-r--r--src/components/user_profile/user_profile.vue3
-rw-r--r--src/i18n/ko.json370
-rw-r--r--src/i18n/messages.js1
-rw-r--r--src/modules/api.js10
-rw-r--r--src/modules/statuses.js127
-rw-r--r--src/modules/users.js12
-rw-r--r--src/services/api/api.service.js33
-rw-r--r--src/services/entity_normalizer/entity_normalizer.service.js212
-rw-r--r--src/services/timeline_fetcher/timeline_fetcher.service.js6
14 files changed, 699 insertions, 132 deletions
diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js
index 95432248..237de7e5 100644
--- a/src/components/conversation/conversation.js
+++ b/src/components/conversation/conversation.js
@@ -1,9 +1,8 @@
import { reduce, filter, sortBy } from 'lodash'
-import { statusType } from '../../modules/statuses.js'
import Status from '../status/status.vue'
const sortAndFilterConversation = (conversation) => {
- conversation = filter(conversation, (status) => statusType(status) !== 'retweet')
+ conversation = filter(conversation, (status) => status.type !== 'retweet')
return sortBy(conversation, 'id')
}
@@ -18,10 +17,12 @@ const conversation = {
'collapsable'
],
computed: {
- status () { return this.statusoid },
+ status () {
+ return this.statusoid
+ },
conversation () {
if (!this.status) {
- return false
+ return []
}
const conversationId = this.status.statusnet_conversation_id
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 7e1e7dab..b648ab70 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -270,7 +270,7 @@ const Status = {
},
replyEnter (id, event) {
this.showPreview = true
- const targetId = String(id)
+ const targetId = id
const statuses = this.$store.state.statuses.allStatuses
if (!this.preview) {
@@ -295,7 +295,6 @@ const Status = {
},
watch: {
'highlight': function (id) {
- id = String(id)
if (this.status.id === id) {
let rect = this.$el.getBoundingClientRect()
if (rect.top < 100) {
diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js
index 98da8660..23d2c1e8 100644
--- a/src/components/timeline/timeline.js
+++ b/src/components/timeline/timeline.js
@@ -7,7 +7,6 @@ import { throttle } from 'lodash'
const Timeline = {
props: [
'timeline',
- 'timelineName',
'title',
'userId',
'tag',
@@ -55,7 +54,7 @@ const Timeline = {
timelineFetcher.fetchAndUpdate({
store,
credentials,
- timeline: this.timelineName,
+ timeline: this.timeline,
showImmediately,
userId: this.userId,
tag: this.tag
@@ -70,32 +69,32 @@ const Timeline = {
destroyed () {
window.removeEventListener('scroll', this.scrollLoad)
if (typeof document.hidden !== 'undefined') document.removeEventListener('visibilitychange', this.handleVisibilityChange, false)
- this.$store.commit('setLoading', { timeline: this.timelineName, value: false })
+ this.$store.commit('setLoading', { timeline: this.timeline, value: false })
},
methods: {
showNewStatuses () {
if (this.timeline.flushMarker !== 0) {
- this.$store.commit('clearTimeline', { timeline: this.timelineName })
- this.$store.commit('queueFlush', { timeline: this.timelineName, id: 0 })
+ this.$store.commit('clearTimeline', { timeline: this.timeline })
+ this.$store.commit('queueFlush', { timeline: this.timeline, id: 0 })
this.fetchOlderStatuses()
} else {
- this.$store.commit('showNewStatuses', { timeline: this.timelineName })
+ this.$store.commit('showNewStatuses', { timeline: this.timeline })
this.paused = false
}
},
fetchOlderStatuses: throttle(function () {
const store = this.$store
const credentials = store.state.users.currentUser.credentials
- store.commit('setLoading', { timeline: this.timelineName, value: true })
+ store.commit('setLoading', { timeline: this.timeline, value: true })
timelineFetcher.fetchAndUpdate({
store,
credentials,
- timeline: this.timelineName,
+ timeline: this.timeline,
older: true,
showImmediately: true,
userId: this.userId,
tag: this.tag
- }).then(() => store.commit('setLoading', { timeline: this.timelineName, value: false }))
+ }).then(() => store.commit('setLoading', { timeline: this.timeline, value: false }))
}, 1000, this),
scrollLoad (e) {
const bodyBRect = document.body.getBoundingClientRect()
diff --git a/src/components/user_card_content/user_card_content.vue b/src/components/user_card_content/user_card_content.vue
index 3804302b..0e820182 100644
--- a/src/components/user_card_content/user_card_content.vue
+++ b/src/components/user_card_content/user_card_content.vue
@@ -142,7 +142,7 @@
border-bottom-right-radius: 0;
.panel-heading {
- padding: .6em 0;
+ padding: .5em 0;
text-align: center;
box-shadow: none;
}
@@ -226,10 +226,11 @@
}
}
- .user-name{
+ .user-name {
text-overflow: ellipsis;
overflow: hidden;
- flex: 1 0 auto;
+ flex: 1 1 auto;
+ margin-right: 1em;
}
.user-screen-name {
@@ -245,6 +246,10 @@
.dailyAvg {
min-width: 1px;
flex: 0 0 auto;
+ margin-left: 1em;
+ font-size: 0.7em;
+ color: $fallback--text;
+ color: var(--text, $fallback--text);
}
.handle {
@@ -381,11 +386,6 @@
}
}
-.dailyAvg {
- margin-left: 1em;
- font-size: 0.7em;
- color: #CCC;
-}
.floater {
}
</style>
diff --git a/src/components/user_profile/user_profile.js b/src/components/user_profile/user_profile.js
index 2ca09817..245d55ca 100644
--- a/src/components/user_profile/user_profile.js
+++ b/src/components/user_profile/user_profile.js
@@ -1,6 +1,7 @@
import UserCardContent from '../user_card_content/user_card_content.vue'
import UserCard from '../user_card/user_card.vue'
import Timeline from '../timeline/timeline.vue'
+import { emptyTl } from '../../modules/statuses.js'
const UserProfile = {
created () {
@@ -13,6 +14,11 @@ const UserProfile = {
destroyed () {
this.$store.dispatch('stopFetching', 'user')
},
+ data () {
+ return {
+ favorites: emptyTl({ type: 'favorites', userId: this.userId })
+ }
+ },
computed: {
timeline () {
return this.$store.state.statuses.timelines.user
@@ -21,7 +27,7 @@ const UserProfile = {
return this.$route.params.id || this.user.id
},
userName () {
- return this.$route.params.name
+ return this.$route.params.name || this.user.screen_name
},
friends () {
return this.user.friends
@@ -68,7 +74,7 @@ const UserProfile = {
}
this.$store.dispatch('stopFetching', 'user')
this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.dispatch('startFetching', ['user', this.userName])
+ this.$store.dispatch('startFetching', ['user', this.fetchBy])
},
userId () {
if (!this.isExternal) {
@@ -76,7 +82,7 @@ const UserProfile = {
}
this.$store.dispatch('stopFetching', 'user')
this.$store.commit('clearTimeline', { timeline: 'user' })
- this.$store.dispatch('startFetching', ['user', this.userId])
+ this.$store.dispatch('startFetching', ['user', this.fetchBy])
},
user () {
if (this.user.id && !this.user.followers) {
diff --git a/src/components/user_profile/user_profile.vue b/src/components/user_profile/user_profile.vue
index a46befa5..265fc65b 100644
--- a/src/components/user_profile/user_profile.vue
+++ b/src/components/user_profile/user_profile.vue
@@ -3,7 +3,7 @@
<div v-if="user.id" class="user-profile panel panel-default">
<user-card-content :user="user" :switcher="true" :selected="timeline.viewing"></user-card-content>
<tab-switcher>
- <Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="userId"/>
+ <Timeline :label="$t('user_card.statuses')" :embedded="true" :title="$t('user_profile.timeline_title')" :timeline="timeline" :timeline-name="'user'" :user-id="fetchBy"/>
<div :label="$t('user_card.followees')">
<div v-if="friends">
<user-card v-for="friend in friends" :key="friend.id" :user="friend" :showFollows="true"></user-card>
@@ -20,6 +20,7 @@
<i class="icon-spin3 animate-spin"></i>
</div>
</div>
+ <Timeline :label="$t('user_card.favorites')" :embedded="true" :title="$t('user_profile.favorites_title')" :timeline="favorites"/>
</tab-switcher>
</div>
<div v-else class="panel user-profile-placeholder">
diff --git a/src/i18n/ko.json b/src/i18n/ko.json
new file mode 100644
index 00000000..4b69df07
--- /dev/null
+++ b/src/i18n/ko.json
@@ -0,0 +1,370 @@
+{
+ "chat": {
+ "title": "챗"
+ },
+ "features_panel": {
+ "chat": "챗",
+ "gopher": "고퍼",
+ "media_proxy": "미디어 프록시",
+ "scope_options": "범위 옵션",
+ "text_limit": "텍스트 제한",
+ "title": "기능",
+ "who_to_follow": "팔로우 추천"
+ },
+ "finder": {
+ "error_fetching_user": "사용자 정보 불러오기 실패",
+ "find_user": "사용자 찾기"
+ },
+ "general": {
+ "apply": "적용",
+ "submit": "보내기"
+ },
+ "login": {
+ "login": "로그인",
+ "description": "OAuth로 로그인",
+ "logout": "로그아웃",
+ "password": "암호",
+ "placeholder": "예시: lain",
+ "register": "가입",
+ "username": "사용자 이름"
+ },
+ "nav": {
+ "about": "About",
+ "back": "뒤로",
+ "chat": "로컬 챗",
+ "friend_requests": "팔로우 요청",
+ "mentions": "멘션",
+ "dms": "다이렉트 메시지",
+ "public_tl": "공개 타임라인",
+ "timeline": "타임라인",
+ "twkn": "모든 알려진 네트워크",
+ "user_search": "사용자 검색",
+ "preferences": "환경설정"
+ },
+ "notifications": {
+ "broken_favorite": "알 수 없는 게시물입니다, 검색 합니다...",
+ "favorited_you": "당신의 게시물을 즐겨찾기",
+ "followed_you": "당신을 팔로우",
+ "load_older": "오래 된 알림 불러오기",
+ "notifications": "알림",
+ "read": "읽음!",
+ "repeated_you": "당신의 게시물을 리핏"
+ },
+ "post_status": {
+ "new_status": "새 게시물 게시",
+ "account_not_locked_warning": "당신의 계정은 {0} 상태가 아닙니다. 누구나 당신을 팔로우 하고 팔로워 전용 게시물을 볼 수 있습니다.",
+ "account_not_locked_warning_link": "잠김",
+ "attachments_sensitive": "첨부물을 민감함으로 설정",
+ "content_type": {
+ "plain_text": "평문"
+ },
+ "content_warning": "주제 (필수 아님)",
+ "default": "LA에 도착!",
+ "direct_warning": "이 게시물을 멘션 된 사용자들에게만 보여집니다",
+ "posting": "게시",
+ "scope": {
+ "direct": "다이렉트 - 멘션 된 사용자들에게만",
+ "private": "팔로워 전용 - 팔로워들에게만",
+ "public": "공개 - 공개 타임라인으로",
+ "unlisted": "비공개 - 공개 타임라인에 게시 안 함"
+ }
+ },
+ "registration": {
+ "bio": "소개",
+ "email": "이메일",
+ "fullname": "표시 되는 이름",
+ "password_confirm": "암호 확인",
+ "registration": "가입하기",
+ "token": "초대 토큰",
+ "captcha": "캡차",
+ "new_captcha": "이미지를 클릭해서 새로운 캡차",
+ "validations": {
+ "username_required": "공백으로 둘 수 없습니다",
+ "fullname_required": "공백으로 둘 수 없습니다",
+ "email_required": "공백으로 둘 수 없습니다",
+ "password_required": "공백으로 둘 수 없습니다",
+ "password_confirmation_required": "공백으로 둘 수 없습니다",
+ "password_confirmation_match": "패스워드와 일치해야 합니다"
+ }
+ },
+ "settings": {
+ "attachmentRadius": "첨부물",
+ "attachments": "첨부물",
+ "autoload": "최하단에 도착하면 자동으로 로드 활성화",
+ "avatar": "아바타",
+ "avatarAltRadius": "아바타 (알림)",
+ "avatarRadius": "아바타",
+ "background": "배경",
+ "bio": "소개",
+ "btnRadius": "버튼",
+ "cBlue": "파랑 (답글, 팔로우)",
+ "cGreen": "초록 (리트윗)",
+ "cOrange": "주황 (즐겨찾기)",
+ "cRed": "빨강 (취소)",
+ "change_password": "암호 바꾸기",
+ "change_password_error": "암호를 바꾸는 데 몇 가지 문제가 있습니다.",
+ "changed_password": "암호를 바꾸었습니다!",
+ "collapse_subject": "주제를 가진 게시물 접기",
+ "composing": "작성",
+ "confirm_new_password": "새 패스워드 확인",
+ "current_avatar": "현재 아바타",
+ "current_password": "현재 패스워드",
+ "current_profile_banner": "현재 프로필 배너",
+ "data_import_export_tab": "데이터 불러오기 / 내보내기",
+ "default_vis": "기본 공개 범위",
+ "delete_account": "계정 삭제",
+ "delete_account_description": "계정과 메시지를 영구히 삭제.",
+ "delete_account_error": "계정을 삭제하는데 문제가 있습니다. 계속 발생한다면 인스턴스 관리자에게 문의하세요.",
+ "delete_account_instructions": "계정 삭제를 확인하기 위해 아래에 패스워드 입력.",
+ "export_theme": "프리셋 저장",
+ "filtering": "필터링",
+ "filtering_explanation": "아래의 단어를 가진 게시물들은 뮤트 됩니다, 한 줄에 하나씩 적으세요",
+ "follow_export": "팔로우 내보내기",
+ "follow_export_button": "팔로우 목록을 csv로 내보내기",
+ "follow_export_processing": "진행 중입니다, 곧 다운로드 가능해 질 것입니다",
+ "follow_import": "팔로우 불러오기",
+ "follow_import_error": "팔로우 불러오기 실패",
+ "follows_imported": "팔로우 목록을 불러왔습니다! 처리에는 시간이 걸립니다.",
+ "foreground": "전경",
+ "general": "일반",
+ "hide_attachments_in_convo": "대화의 첨부물 숨기기",
+ "hide_attachments_in_tl": "타임라인의 첨부물 숨기기",
+ "hide_isp": "인스턴스 전용 패널 숨기기",
+ "preload_images": "이미지 미리 불러오기",
+ "hide_post_stats": "게시물 통계 숨기기 (즐겨찾기 수 등)",
+ "hide_user_stats": "사용자 통계 숨기기 (팔로워 수 등)",
+ "import_followers_from_a_csv_file": "csv 파일에서 팔로우 목록 불러오기",
+ "import_theme": "프리셋 불러오기",
+ "inputRadius": "입력 칸",
+ "checkboxRadius": "체크박스",
+ "instance_default": "(기본: {value})",
+ "instance_default_simple": "(기본)",
+ "interface": "인터페이스",
+ "interfaceLanguage": "인터페이스 언어",
+ "invalid_theme_imported": "선택한 파일은 지원하는 플레로마 테마가 아닙니다. 아무런 변경도 일어나지 않았습니다.",
+ "limited_availability": "이 브라우저에서 사용 불가",
+ "links": "링크",
+ "lock_account_description": "계정을 승인 된 팔로워들로 제한",
+ "loop_video": "비디오 반복재생",
+ "loop_video_silent_only": "소리가 없는 비디오만 반복 재생 (마스토돈의 \"gifs\" 같은 것들)",
+ "name": "이름",
+ "name_bio": "이름 & 소개",
+ "new_password": "새 암호",
+ "notification_visibility": "보여 줄 알림 종류",
+ "notification_visibility_follows": "팔로우",
+ "notification_visibility_likes": "좋아함",
+ "notification_visibility_mentions": "멘션",
+ "notification_visibility_repeats": "반복",
+ "no_rich_text_description": "모든 게시물의 서식을 지우기",
+ "hide_network_description": "내 팔로우와 팔로워를 숨기기",
+ "nsfw_clickthrough": "NSFW 이미지 \"클릭해서 보이기\"를 활성화",
+ "panelRadius": "패널",
+ "pause_on_unfocused": "탭이 활성 상태가 아닐 때 스트리밍 멈추기",
+ "presets": "프리셋",
+ "profile_background": "프로필 배경",
+ "profile_banner": "프로필 배너",
+ "profile_tab": "프로필",
+ "radii_help": "인터페이스 모서리 둥글기 (픽셀 단위)",
+ "replies_in_timeline": "답글을 타임라인에",
+ "reply_link_preview": "마우스를 올려서 답글 링크 미리보기 활성화",
+ "reply_visibility_all": "모든 답글 보기",
+ "reply_visibility_following": "나에게 직접 오는 답글이나 내가 팔로우 중인 사람에게서 오는 답글만 표시",
+ "reply_visibility_self": "나에게 직접 전송 된 답글만 보이기",
+ "saving_err": "설정 저장 실패",
+ "saving_ok": "설정 저장 됨",
+ "security_tab": "보안",
+ "scope_copy": "답글을 달 때 공개 범위 따라가리 (다이렉트 메시지는 언제나 따라감)",
+ "set_new_avatar": "새 아바타 설정",
+ "set_new_profile_background": "새 프로필 배경 설정",
+ "set_new_profile_banner": "새 프로필 배너 설정",
+ "settings": "설정",
+ "subject_input_always_show": "항상 주제 칸 보이기",
+ "subject_line_behavior": "답글을 달 때 주제 복사하기",
+ "subject_line_email": "이메일처럼: \"re: 주제\"",
+ "subject_line_mastodon": "마스토돈처럼: 그대로 복사",
+ "subject_line_noop": "복사 안 함",
+ "stop_gifs": "GIF파일에 마우스를 올려서 재생",
+ "streaming": "최상단에 도달하면 자동으로 새 게시물 스트리밍",
+ "text": "텍스트",
+ "theme": "테마",
+ "theme_help": "16진수 색상코드(#rrggbb)를 사용해 색상 테마를 커스터마이즈.",
+ "theme_help_v2_1": "체크박스를 통해 몇몇 컴포넌트의 색상과 불투명도를 조절 가능, \"모두 지우기\" 버튼으로 덮어 씌운 것을 모두 취소.",
+ "theme_help_v2_2": "몇몇 입력칸 밑의 아이콘은 전경/배경 대비 관련 표시등입니다, 마우스를 올려 자세한 정보를 볼 수 있습니다. 투명도 대비 표시등이 가장 최악의 경우를 나타낸다는 것을 유의하세요.",
+ "tooltipRadius": "툴팁/경고",
+ "user_settings": "사용자 설정",
+ "values": {
+ "false": "아니오",
+ "true": "네"
+ },
+ "notifications": "알림",
+ "enable_web_push_notifications": "웹 푸시 알림 활성화",
+ "style": {
+ "switcher": {
+ "keep_color": "색상 유지",
+ "keep_shadows": "그림자 유지",
+ "keep_opacity": "불투명도 유지",
+ "keep_roundness": "둥글기 유지",
+ "keep_fonts": "글자체 유지",
+ "save_load_hint": "\"유지\" 옵션들은 다른 테마를 고르거나 불러 올 때 현재 설정 된 옵션들을 건드리지 않게 합니다, 테마를 내보내기 할 때도 이 옵션에 따라 저장합니다. 아무 것도 체크 되지 않았다면 모든 설정을 내보냅니다.",
+ "reset": "초기화",
+ "clear_all": "모두 지우기",
+ "clear_opacity": "불투명도 지우기"
+ },
+ "common": {
+ "color": "색상",
+ "opacity": "불투명도",
+ "contrast": {
+ "hint": "대비율이 {ratio}입니다, 이것은 {context} {level}",
+ "level": {
+ "aa": "AA등급 가이드라인에 부합합니다 (최소한도)",
+ "aaa": "AAA등급 가이드라인에 부합합니다 (권장)",
+ "bad": "아무런 가이드라인 등급에도 미치지 못합니다"
+ },
+ "context": {
+ "18pt": "큰 (18pt 이상) 텍스트에 대해",
+ "text": "텍스트에 대해"
+ }
+ }
+ },
+ "common_colors": {
+ "_tab_label": "일반",
+ "main": "일반 색상",
+ "foreground_hint": "\"고급\" 탭에서 더 자세한 설정이 가능합니다",
+ "rgbo": "아이콘, 강조, 배지"
+ },
+ "advanced_colors": {
+ "_tab_label": "고급",
+ "alert": "주의 배경",
+ "alert_error": "에러",
+ "badge": "배지 배경",
+ "badge_notification": "알림",
+ "panel_header": "패널 헤더",
+ "top_bar": "상단 바",
+ "borders": "테두리",
+ "buttons": "버튼",
+ "inputs": "입력칸",
+ "faint_text": "흐려진 텍스트"
+ },
+ "radii": {
+ "_tab_label": "둥글기"
+ },
+ "shadows": {
+ "_tab_label": "그림자와 빛",
+ "component": "컴포넌트",
+ "override": "덮어쓰기",
+ "shadow_id": "그림자 #{value}",
+ "blur": "흐리기",
+ "spread": "퍼지기",
+ "inset": "안쪽으로",
+ "hint": "그림자에는 CSS3 변수를 --variable을 통해 색상 값으로 사용할 수 있습니다. 불투명도에는 적용 되지 않습니다.",
+ "filter_hint": {
+ "always_drop_shadow": "경고, 이 그림자는 브라우저가 지원하는 경우 항상 {0}을 사용합니다.",
+ "drop_shadow_syntax": "{0}는 {1} 파라미터와 {2} 키워드를 지원하지 않습니다.",
+ "avatar_inset": "안쪽과 안쪽이 아닌 그림자를 모두 설정하는 경우 투명 아바타에서 예상치 못 한 결과가 나올 수 있다는 것에 주의해 주세요.",
+ "spread_zero": "퍼지기가 0보다 큰 그림자는 0으로 설정한 것과 동일하게 보여집니다",
+ "inset_classic": "안쪽 그림자는 {0}를 사용합니다"
+ },
+ "components": {
+ "panel": "패널",
+ "panelHeader": "패널 헤더",
+ "topBar": "상단 바",
+ "avatar": "사용자 아바타 (프로필 뷰에서)",
+ "avatarStatus": "사용자 아바타 (게시물에서)",
+ "popup": "팝업과 툴팁",
+ "button": "버튼",
+ "buttonHover": "버튼 (마우스 올렸을 때)",
+ "buttonPressed": "버튼 (눌렸을 때)",
+ "buttonPressedHover": "Button (마우스 올림 + 눌림)",
+ "input": "입력칸"
+ }
+ },
+ "fonts": {
+ "_tab_label": "글자체",
+ "help": "인터페이스의 요소에 사용 될 글자체를 고르세요. \"커스텀\"은 시스템에 있는 폰트 이름을 정확히 입력해야 합니다.",
+ "components": {
+ "interface": "인터페이스",
+ "input": "입력칸",
+ "post": "게시물 텍스트",
+ "postCode": "게시물의 고정폭 텍스트 (서식 있는 텍스트)"
+ },
+ "family": "글자체 이름",
+ "size": "크기 (px 단위)",
+ "weight": "굵기",
+ "custom": "커스텀"
+ },
+ "preview": {
+ "header": "미리보기",
+ "content": "내용",
+ "error": "에러 예시",
+ "button": "버튼",
+ "text": "더 많은 {0} 그리고 {1}",
+ "mono": "내용",
+ "input": "LA에 막 도착!",
+ "faint_link": "도움 되는 설명서",
+ "fine_print": "우리의 {0} 를 읽고 도움 되지 않는 것들을 배우자!",
+ "header_faint": "이건 괜찮아",
+ "checkbox": "나는 약관을 대충 훑어보았습니다",
+ "link": "작고 귀여운 링크"
+ }
+ }
+ },
+ "timeline": {
+ "collapse": "접기",
+ "conversation": "대화",
+ "error_fetching": "업데이트 불러오기 실패",
+ "load_older": "더 오래 된 게시물 불러오기",
+ "no_retweet_hint": "팔로워 전용, 다이렉트 메시지는 반복할 수 없습니다",
+ "repeated": "반복 됨",
+ "show_new": "새로운 것 보기",
+ "up_to_date": "최신 상태"
+ },
+ "user_card": {
+ "approve": "승인",
+ "block": "차단",
+ "blocked": "차단 됨!",
+ "deny": "거부",
+ "follow": "팔로우",
+ "follow_sent": "요청 보내짐!",
+ "follow_progress": "요청 중…",
+ "follow_again": "요청을 다시 보낼까요?",
+ "follow_unfollow": "팔로우 중지",
+ "followees": "팔로우 중",
+ "followers": "팔로워",
+ "following": "팔로우 중!",
+ "follows_you": "당신을 팔로우 합니다!",
+ "its_you": "당신입니다!",
+ "mute": "침묵",
+ "muted": "침묵 됨",
+ "per_day": " / 하루",
+ "remote_follow": "원격 팔로우",
+ "statuses": "게시물"
+ },
+ "user_profile": {
+ "timeline_title": "사용자 타임라인"
+ },
+ "who_to_follow": {
+ "more": "더 보기",
+ "who_to_follow": "팔로우 추천"
+ },
+ "tool_tip": {
+ "media_upload": "미디어 업로드",
+ "repeat": "반복",
+ "reply": "답글",
+ "favorite": "즐겨찾기",
+ "user_settings": "사용자 설정"
+ },
+ "upload":{
+ "error": {
+ "base": "업로드 실패.",
+ "file_too_big": "파일이 너무 커요 [{filesize}{filesizeunit} / {allowedsize}{allowedsizeunit}]",
+ "default": "잠시 후에 다시 시도해 보세요"
+ },
+ "file_size_units": {
+ "B": "바이트",
+ "KiB": "키비바이트",
+ "MiB": "메비바이트",
+ "GiB": "기비바이트",
+ "TiB": "테비바이트"
+ }
+ }
+}
diff --git a/src/i18n/messages.js b/src/i18n/messages.js
index ee08db44..7a57648e 100644
--- a/src/i18n/messages.js
+++ b/src/i18n/messages.js
@@ -22,6 +22,7 @@ const messages = {
hu: require('./hu.json'),
it: require('./it.json'),
ja: require('./ja.json'),
+ ko: require('./ko.json'),
nb: require('./nb.json'),
oc: require('./oc.json'),
pl: require('./pl.json'),
diff --git a/src/modules/api.js b/src/modules/api.js
index a61340c2..b85b24be 100644
--- a/src/modules/api.js
+++ b/src/modules/api.js
@@ -5,7 +5,7 @@ import { Socket } from 'phoenix'
const api = {
state: {
backendInteractor: backendInteractorService(),
- fetchers: {},
+ fetchers: new Map(),
socket: null,
chatDisabled: false,
followRequests: []
@@ -15,10 +15,10 @@ const api = {
state.backendInteractor = backendInteractor
},
addFetcher (state, {timeline, fetcher}) {
- state.fetchers[timeline] = fetcher
+ state.fetchers.set(timeline, fetcher)
},
removeFetcher (state, {timeline}) {
- delete state.fetchers[timeline]
+ delete state.fetchers.delete(timeline)
},
setSocket (state, socket) {
state.socket = socket
@@ -41,13 +41,13 @@ const api = {
}
// Don't start fetching if we already are.
- if (!store.state.fetchers[timeline]) {
+ if (!store.state.fetchers.has(timeline)) {
const fetcher = store.state.backendInteractor.startFetching({timeline, store, userId})
store.commit('addFetcher', {timeline, fetcher})
}
},
stopFetching (store, timeline) {
- const fetcher = store.state.fetchers[timeline]
+ const fetcher = store.state.fetchers.get(timeline)
window.clearInterval(fetcher)
store.commit('removeFetcher', {timeline})
},
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index c564dec1..baeef8bf 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,8 +1,8 @@
-import { includes, remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'lodash'
+import { remove, slice, each, find, maxBy, minBy, merge, last, isArray } from 'lodash'
import apiService from '../services/api/api.service.js'
// import parse from '../services/status_parser/status_parser.js'
-const emptyTl = () => ({
+export const emptyTl = (tl, userId = 0) => (Object.assign(tl, {
statuses: [],
statusesObject: {},
faves: [],
@@ -14,9 +14,9 @@ const emptyTl = () => ({
loading: false,
followers: [],
friends: [],
- userId: 0,
- flushMarker: 0
-})
+ flushMarker: 0,
+ userId
+}))
export const defaultState = {
allStatuses: [],
@@ -33,30 +33,17 @@ export const defaultState = {
favorites: new Set(),
error: false,
timelines: {
- mentions: emptyTl(),
- public: emptyTl(),
- user: emptyTl(),
- publicAndExternal: emptyTl(),
- friends: emptyTl(),
- tag: emptyTl(),
- dms: emptyTl()
+ mentions: emptyTl({ type: 'mentions' }),
+ public: emptyTl({ type: 'public' }),
+ user: emptyTl({ type: 'user' }), // TODO: switch to unregistered
+ publicAndExternal: emptyTl({ type: 'publicAndExternal' }),
+ friends: emptyTl({ type: 'friends' }),
+ tag: emptyTl({ type: 'tag' }),
+ dms: emptyTl({ type: 'dms' })
}
}
-const isNsfw = (status) => {
- const nsfwRegex = /#nsfw/i
- return includes(status.tags, 'nsfw') || !!status.text.match(nsfwRegex)
-}
-
export const prepareStatus = (status) => {
- // Parse nsfw tags
- if (status.nsfw === undefined) {
- status.nsfw = isNsfw(status)
- if (status.retweeted_status) {
- status.nsfw = status.retweeted_status.nsfw
- }
- }
-
// Set deleted flag
status.deleted = false
@@ -75,31 +62,6 @@ const visibleNotificationTypes = (rootState) => {
].filter(_ => _)
}
-export const statusType = (status) => {
- if (status.is_post_verb) {
- return 'status'
- }
-
- if (status.retweeted_status) {
- return 'retweet'
- }
-
- if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
- (typeof status.text === 'string' && status.text.match(/favorited/))) {
- return 'favorite'
- }
-
- if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
- return 'deletion'
- }
-
- if (status.text.match(/started following/) || status.activity_type === 'follow') {
- return 'follow'
- }
-
- return 'unknown'
-}
-
const mergeOrAdd = (arr, obj, item) => {
// For sequential IDs BE passes numbers as numbers, we want them as strings.
item.id = String(item.id)
@@ -138,7 +100,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
- const timelineObject = state.timelines[timeline]
+ const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline]
const maxNew = statuses.length > 0 ? maxBy(statuses, 'id').id : 0
const older = timeline && maxNew < timelineObject.maxId
@@ -154,13 +116,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
return
}
- const addStatus = (status, showImmediately, addToTimeline = true) => {
- const result = mergeOrAdd(allStatuses, allStatusesObject, status)
- status = result.item
+ const addStatus = (data, showImmediately, addToTimeline = true) => {
+ const result = mergeOrAdd(allStatuses, allStatusesObject, data)
+ const status = result.item
if (result.new) {
// We are mentioned in a post
- if (statusType(status) === 'status' && find(status.attentions, { id: user.id })) {
+ if (status.type === 'status' && find(status.attentions, { id: user.id })) {
const mentions = state.timelines.mentions
// Add the mention to the mentions timeline
@@ -264,6 +226,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
remove(timelineObject.visibleStatuses, { uri })
}
},
+ 'follow': (follow) => {
+ // NOOP, it is known status but we don't do anything about it for now
+ },
'default': (unknown) => {
console.log('unknown status type')
console.log(unknown)
@@ -271,7 +236,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
}
each(statuses, (status) => {
- const type = statusType(status)
+ const type = status.type
const processor = processors[type] || processors['default']
processor(status)
})
@@ -289,15 +254,11 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
const allStatuses = state.allStatuses
const allStatusesObject = state.allStatusesObject
each(notifications, (notification) => {
- notification.notice.id = String(notification.notice.id)
- const result = mergeOrAdd(allStatuses, allStatusesObject, notification.notice)
- const action = result.item
- // For sequential IDs BE passes numbers as numbers, we want them as strings.
- action.id = String(action.id)
+ notification.action = mergeOrAdd(allStatuses, allStatusesObject, notification.action).item
+ notification.status = notification.status && mergeOrAdd(allStatuses, allStatusesObject, notification.status).item
// Only add a new notification if we don't have one for the same action
- // TODO: this technically works but should be checking for notification.id, not action.id i think
- if (!find(state.notifications.data, (oldNotification) => oldNotification.action.id === action.id)) {
+ if (!state.notifications.idStore.hasOwnProperty(notification.id)) {
state.notifications.maxId = notification.id > state.notifications.maxId
? notification.id
: state.notifications.maxId
@@ -305,35 +266,24 @@ const addNewNotifications = (state, { dispatch, notifications, older, visibleNot
? notification.id
: state.notifications.minId
- const fresh = !notification.is_seen
- const status = notification.ntype === 'like'
- ? action.favorited_status
- : action
-
- const result = {
- type: notification.ntype,
- status,
- action,
- seen: !fresh
- }
-
- state.notifications.data.push(result)
- state.notifications.idStore[notification.id] = result
+ state.notifications.data.push(notification)
+ state.notifications.idStore[notification.id] = notification
if ('Notification' in window && window.Notification.permission === 'granted') {
+ const notifObj = {}
+ const action = notification.action
const title = action.user.name
- const result = {}
- result.icon = action.user.profile_image_url
- result.body = action.text // there's a problem that it doesn't put a space before links tho
+ notifObj.icon = action.user.profile_image_url
+ notifObj.body = action.text // there's a problem that it doesn't put a space before links tho
// Shows first attached non-nsfw image, if any. Should add configuration for this somehow...
if (action.attachments && action.attachments.length > 0 && !action.nsfw &&
action.attachments[0].mimetype.startsWith('image/')) {
- result.image = action.attachments[0].url
+ notifObj.image = action.attachments[0].url
}
- if (fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
- let notification = new window.Notification(title, result)
+ if (notification.fresh && !state.notifications.desktopNotificationSilence && visibleNotificationTypes.includes(notification.ntype)) {
+ let notification = new window.Notification(title, notifObj)
// Chrome is known for not closing notifications automatically
// according to MDN, anyway.
setTimeout(notification.close.bind(notification), 5000)
@@ -347,7 +297,7 @@ export const mutations = {
addNewStatuses,
addNewNotifications,
showNewStatuses (state, { timeline }) {
- const oldTimeline = (state.timelines[timeline])
+ const oldTimeline = (typeof timeline === 'object' ? timeline : state.timelines[timeline])
oldTimeline.newStatusCount = 0
oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
@@ -356,7 +306,8 @@ export const mutations = {
each(oldTimeline.visibleStatuses, (status) => { oldTimeline.visibleStatusesObject[status.id] = status })
},
clearTimeline (state, { timeline }) {
- state.timelines[timeline] = emptyTl()
+ const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline]
+ emptyTl(timelineObject, timeline.userId)
},
setFavorited (state, { status, value }) {
const newStatus = state.allStatusesObject[status.id]
@@ -376,7 +327,8 @@ export const mutations = {
newStatus.deleted = true
},
setLoading (state, { timeline, value }) {
- state.timelines[timeline].loading = value
+ const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline]
+ timelineObject.loading = value
},
setNsfw (state, { id, nsfw }) {
const newStatus = state.allStatusesObject[id]
@@ -397,7 +349,8 @@ export const mutations = {
})
},
queueFlush (state, { timeline, id }) {
- state.timelines[timeline].flushMarker = id
+ const timelineObject = typeof timeline === 'object' ? timeline : state.timelines[timeline]
+ timelineObject.flushMarker = id
}
}
diff --git a/src/modules/users.js b/src/modules/users.js
index 9d00b782..33c02a07 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -7,9 +7,6 @@ import { humanizeErrors } from './errors'
// TODO: Unify with mergeOrAdd in statuses.js
export const mergeOrAdd = (arr, obj, item) => {
- // For sequential IDs BE passes numbers as numbers, we want them as strings.
- item.id = String(item.id)
-
if (!item) { return false }
const oldItem = obj[item.id]
if (oldItem) {
@@ -67,10 +64,11 @@ export const mutations = {
each(users, (user) => mergeOrAdd(state.users, state.usersObject, user))
},
setUserForStatus (state, status) {
- status.user = state.usersObject[String(status.user.id)]
+ status.user = state.usersObject[status.user.id]
},
setUserForNotification (state, notification) {
- notification.action.user = state.usersObject[String(notification.action.user.id)]
+ notification.action.user = state.usersObject[notification.action.user.id]
+ notification.from_profile = state.usersObject[notification.action.user.id]
},
setColor (state, { user: { id }, highlighted }) {
const user = state.usersObject[id]
@@ -152,8 +150,8 @@ const users = {
})
},
addNewNotifications (store, { notifications }) {
- const users = compact(map(notifications, 'from_profile'))
- const notificationIds = compact(notifications.map(_ => String(_.id)))
+ const users = map(notifications, 'from_profile')
+ const notificationIds = notifications.map(_ => String(_.id))
store.commit('addNewUsers', users)
const notificationsObject = store.rootState.statuses.notifications.idStore
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 4ee95bd1..14a526ef 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -41,7 +41,10 @@ const APPROVE_USER_URL = '/api/pleroma/friendships/approve'
const DENY_USER_URL = '/api/pleroma/friendships/deny'
const SUGGESTIONS_URL = '/api/v1/suggestions'
+const MASTODON_USER_FAVORITES_TIMELINE_URL = '/api/v1/favourites'
+
import { each, map } from 'lodash'
+import { parseStatus, parseUser, parseNotification } from '../entity_normalizer/entity_normalizer.service.js'
import 'whatwg-fetch'
const oldfetch = window.fetch
@@ -70,6 +73,7 @@ const updateAvatar = ({credentials, params}) => {
form.append(key, value)
}
})
+
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
@@ -87,6 +91,7 @@ const updateBg = ({credentials, params}) => {
form.append(key, value)
}
})
+
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
@@ -110,6 +115,7 @@ const updateBanner = ({credentials, params}) => {
form.append(key, value)
}
})
+
return fetch(url, {
headers: authHeaders(credentials),
method: 'POST',
@@ -237,24 +243,28 @@ const fetchUser = ({id, credentials}) => {
let url = `${USER_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => parseUser(data))
}
const fetchFriends = ({id, credentials}) => {
let url = `${FRIENDS_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const fetchFollowers = ({id, credentials}) => {
let url = `${FOLLOWERS_URL}?user_id=${id}`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const fetchAllFollowing = ({username, credentials}) => {
const url = `${ALL_FOLLOWING_URL}/${username}.json`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => data.map(parseUser))
}
const fetchFollowRequests = ({credentials}) => {
@@ -267,12 +277,26 @@ const fetchConversation = ({id, credentials}) => {
let url = `${CONVERSATION_URL}/${id}.json?count=100`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => {
+ if (data.ok) {
+ return data
+ }
+ throw new Error('Error fetching timeline')
+ })
+ .then((data) => data.map(parseStatus))
}
const fetchStatus = ({id, credentials}) => {
let url = `${STATUS_URL}/${id}.json`
return fetch(url, { headers: authHeaders(credentials) })
.then((data) => data.json())
+ .then((data) => {
+ if (data.ok) {
+ return data
+ }
+ throw new Error('Error fetching timeline')
+ })
+ .then((data) => parseStatus(data))
}
const setUserMute = ({id, credentials, muted = true}) => {
@@ -300,12 +324,14 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
notifications: QVITTER_USER_NOTIFICATIONS_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
user: QVITTER_USER_TIMELINE_URL,
+ favorites: MASTODON_USER_FAVORITES_TIMELINE_URL,
tag: TAG_TIMELINE_URL
}
+ const type = timeline.type || timeline
+ const isNotifications = type === 'notifications'
+ const params = []
- let url = timelineUrls[timeline]
-
- let params = []
+ let url = timelineUrls[type]
if (since) {
params.push(['since_id', since])
@@ -333,6 +359,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
throw new Error('Error fetching timeline')
})
.then((data) => data.json())
+ .then((data) => data.map(isNotifications ? parseNotification : parseStatus))
}
const verifyCredentials = (user) => {
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
new file mode 100644
index 00000000..adc7f047
--- /dev/null
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -0,0 +1,212 @@
+const qvitterStatusType = (status) => {
+ if (status.is_post_verb) {
+ return 'status'
+ }
+
+ if (status.retweeted_status) {
+ return 'retweet'
+ }
+
+ if ((typeof status.uri === 'string' && status.uri.match(/(fave|objectType=Favourite)/)) ||
+ (typeof status.text === 'string' && status.text.match(/favorited/))) {
+ return 'favorite'
+ }
+
+ if (status.text.match(/deleted notice {{tag/) || status.qvitter_delete_notice) {
+ return 'deletion'
+ }
+
+ if (status.text.match(/started following/) || status.activity_type === 'follow') {
+ return 'follow'
+ }
+
+ return 'unknown'
+}
+
+export const parseUser = (data) => {
+ const output = {}
+ const masto = data.hasOwnProperty('acct')
+ // case for users in "mentions" property for statuses in MastoAPI
+ const mastoShort = masto && !data.hasOwnProperty('avatar')
+
+ output.id = data.id
+
+ if (masto) {
+ output.screen_name = data.acct
+
+ // There's nothing else to get
+ if (mastoShort) {
+ return output
+ }
+
+ output.name = null // missing
+ output.name_html = data.display_name
+
+ output.description = null // missing
+ output.description_html = data.note
+
+ // Utilize avatar_static for gif avatars?
+ output.profile_image_url = data.avatar
+ output.profile_image_url_original = data.avatar
+
+ // Same, utilize header_static?
+ output.cover_photo = data.header
+
+ output.friends_count = data.following_count
+
+ output.bot = data.bot
+
+ output.statusnet_profile_url = data.url
+
+ // Missing, trying to recover
+ output.is_local = !output.screen_name.includes('@')
+ } else {
+ output.screen_name = data.screen_name
+
+ output.name = data.name
+ output.name_html = data.name_html
+
+ output.description = data.description
+ output.description_html = data.description_html
+
+ output.profile_image_url = data.profile_image_url
+ output.profile_image_url_original = data.profile_image_url_original
+
+ output.cover_photo = data.cover_photo
+
+ output.friends_count = data.friends_count
+
+ output.bot = null // missing
+
+ output.statusnet_profile_url = data.statusnet_profile_url
+ output.is_local = data.is_local
+ }
+
+ output.created_at = new Date(data.created_at)
+ output.locked = data.locked
+ output.followers_count = data.followers_count
+ output.statuses_count = data.statuses_count
+
+ return output
+}
+
+const parseAttachment = (data) => {
+ // TODO A little bit messy ATM but works with both APIs
+ return {
+ ...data,
+ mimetype: data.mimetype || data.type
+ }
+}
+
+export const parseStatus = (data) => {
+ const output = {}
+ const masto = data.hasOwnProperty('account')
+
+ if (masto) {
+ output.favorited = data.favourited
+ output.fave_num = data.favourites_count
+
+ output.repeated = data.reblogged
+ output.repeat_num = data.reblogs_count
+
+ output.type = data.reblog ? 'retweet' : 'status'
+ output.nsfw = data.sensitive
+
+ output.statusnet_html = data.content
+
+ // Not exactly the same but works?
+ output.text = data.content
+
+ output.in_reply_to_status_id = data.in_reply_to_id
+ output.in_reply_to_user_id = data.in_reply_to_user_id
+
+ // Not exactly the same but works
+ output.statusnet_conversation_id = data.id
+ } else {
+ output.favorited = data.favorited
+ output.fave_num = data.fave_num
+
+ output.repeated = data.repeated
+ output.repeat_num = data.repeat_num
+
+ // catchall, temporary
+ // Object.assign(output, data)
+
+ output.type = qvitterStatusType(data)
+
+ if (data.nsfw === undefined) {
+ output.nsfw = isNsfw(data)
+ if (data.retweeted_status) {
+ output.nsfw = data.retweeted_status.nsfw
+ }
+ } else {
+ output.nsfw = data.nsfw
+ }
+
+ output.statusnet_html = data.statusnet_html
+ output.text = data.text
+
+ output.in_reply_to_status_id = data.in_reply_to_id
+ output.in_reply_to_user_id = data.in_reply_to_account_id
+
+ output.statusnet_conversation_id = data.statusnet_conversation_id
+ }
+
+ output.id = Number(data.id)
+ output.visibility = data.visibility
+ output.created_at = new Date(data.created_at)
+
+ output.user = parseUser(masto ? data.account : data.user)
+
+ output.attentions = ((masto ? data.mentions : data.attentions) || [])
+ .map(_ => ({
+ id: _.id,
+ following: _.following // FIXME: MastoAPI doesn't have this
+ }))
+
+ output.attachments = ((masto ? data.media_attachments : data.attachments) || [])
+ .map(parseAttachment)
+
+ const retweetedStatus = masto ? data.reblog : data.retweeted_status
+ if (retweetedStatus) {
+ output.retweeted_status = parseStatus(retweetedStatus)
+ }
+
+ return output
+}
+
+export const parseNotification = (data) => {
+ const mastoDict = {
+ 'favourite': 'like',
+ 'reblog': 'repeat'
+ }
+ const masto = !data.hasOwnProperty('ntype')
+ const output = {}
+
+ if (masto) {
+ output.type = mastoDict[data.type] || data.type
+ output.seen = null // missing
+ output.status = parseStatus(data.status)
+ output.action = null // missing
+ output.from_profile = parseUser(data.account)
+ } else {
+ const parsedNotice = parseStatus(data.notice)
+ output.type = data.ntype
+ output.seen = data.is_seen
+ output.status = output.type === 'like'
+ ? parseStatus(data.notice.favorited_status)
+ : parsedNotice
+ output.action = parsedNotice
+ output.from_profile = parseUser(data.from_profile)
+ }
+
+ output.created_at = new Date(data.created_at)
+ output.id = data.id
+
+ return output
+}
+
+const isNsfw = (status) => {
+ const nsfwRegex = /#nsfw/i
+ return (status.tags || []).includes('nsfw') || !!status.text.match(nsfwRegex)
+}
diff --git a/src/services/timeline_fetcher/timeline_fetcher.service.js b/src/services/timeline_fetcher/timeline_fetcher.service.js
index c2a7de56..126e07cf 100644
--- a/src/services/timeline_fetcher/timeline_fetcher.service.js
+++ b/src/services/timeline_fetcher/timeline_fetcher.service.js
@@ -3,7 +3,7 @@ import { camelCase } from 'lodash'
import apiService from '../api/api.service.js'
const update = ({store, statuses, timeline, showImmediately, userId}) => {
- const ccTimeline = camelCase(timeline)
+ const ccTimeline = typeof timeline === 'object' ? timeline : camelCase(timeline)
store.dispatch('setError', { value: false })
@@ -18,7 +18,7 @@ const update = ({store, statuses, timeline, showImmediately, userId}) => {
const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false, showImmediately = false, userId = false, tag = false, until}) => {
const args = { timeline, credentials }
const rootState = store.rootState || store.state
- const timelineData = rootState.statuses.timelines[camelCase(timeline)]
+ const timelineData = typeof timeline === 'object' ? timeline : rootState.statuses.timelines[camelCase(timeline)]
if (older) {
args['until'] = until || timelineData.minVisibleId
@@ -31,7 +31,7 @@ const fetchAndUpdate = ({store, credentials, timeline = 'friends', older = false
return apiService.fetchTimeline(args)
.then((statuses) => {
- if (!older && statuses.length >= 20 && !timelineData.loading) {
+ if (!older && statuses.length >= 20 && !timelineData.loading && timelineData.statuses.length) {
store.dispatch('queueFlush', { timeline: timeline, id: timelineData.maxId })
}
update({store, statuses, timeline, showImmediately, userId})