Repository: NanaMorse/Cuckoo.Plus Branch: develop Commit: 20f4bc6832ac Files: 95 Total size: 330.8 KB Directory structure: gitextract_m6ecmic0/ ├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── deploy.sh ├── light-sail-cuckoo-plus.pem.enc ├── package.json ├── public/ │ ├── index.html │ ├── manifest.json │ └── sw.js ├── server.js ├── src/ │ ├── App.vue │ ├── api/ │ │ ├── accounts.ts │ │ ├── apps.ts │ │ ├── index.ts │ │ ├── instances.ts │ │ ├── lists.ts │ │ ├── media.ts │ │ ├── notifications.ts │ │ ├── oauth.ts │ │ ├── search.ts │ │ ├── statuses.ts │ │ ├── streaming.ts │ │ └── timelines.ts │ ├── components/ │ │ ├── Drawer/ │ │ │ ├── PeopleResultCard.vue │ │ │ ├── Search.vue │ │ │ └── index.vue │ │ ├── Header.vue │ │ ├── Input.vue │ │ ├── Notifications/ │ │ │ ├── Card.vue │ │ │ └── index.vue │ │ ├── PostStatusDialog.vue │ │ ├── StatusCard/ │ │ │ ├── CardHeader.vue │ │ │ ├── FullActionBar.vue │ │ │ ├── FullReplyListItem.vue │ │ │ ├── LinkPreviewPanel.vue │ │ │ ├── MediaPanel.vue │ │ │ ├── PlaceHolderMediaItem.vue │ │ │ ├── SimpleActionBar.vue │ │ │ └── index.vue │ │ ├── ThemeEditPanel.vue │ │ └── VisibilitySelectPopOver.vue │ ├── constant/ │ │ ├── i18n.ts │ │ ├── index.ts │ │ └── routers.ts │ ├── directives.ts │ ├── formatter.spec.ts │ ├── formatter.ts │ ├── i18n/ │ │ ├── compare │ │ ├── de.ts │ │ ├── en.ts │ │ ├── index.ts │ │ ├── ja.ts │ │ ├── zh-cn.ts │ │ ├── zh-hk.ts │ │ └── zh-tw.ts │ ├── index.ts │ ├── interface/ │ │ ├── definition/ │ │ │ ├── vue-extend.d.ts │ │ │ └── vue-shims.d.ts │ │ ├── entities.ts │ │ ├── index.ts │ │ └── store.ts │ ├── pages/ │ │ ├── Accounts/ │ │ │ ├── AccountHeader.vue │ │ │ └── index.vue │ │ ├── OAuth.vue │ │ ├── Settings.vue │ │ ├── Statuses.vue │ │ └── Timelines/ │ │ ├── NewStatusNoticeButton.vue │ │ ├── PostStatusStampCard.vue │ │ └── index.vue │ ├── router/ │ │ └── index.ts │ ├── store/ │ │ ├── actions/ │ │ │ ├── accounts.ts │ │ │ ├── appstatus.ts │ │ │ ├── index.ts │ │ │ ├── notifications.ts │ │ │ ├── relationships.ts │ │ │ ├── statuses.ts │ │ │ └── timelines.ts │ │ ├── getters/ │ │ │ └── index.ts │ │ ├── index.ts │ │ └── mutations/ │ │ ├── appstatus.ts │ │ ├── index.ts │ │ ├── notifications.ts │ │ └── timelines.ts │ ├── themes/ │ │ ├── basecolor.ts │ │ ├── index.ts │ │ ├── presets/ │ │ │ ├── cuckoohub.ts │ │ │ ├── dark.ts │ │ │ ├── googleplus.ts │ │ │ └── greenlight.ts │ │ └── stylepattern.ts │ └── util.ts ├── tsconfig.json └── webpack.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] end_of_line = lf [*.{js,ts,vue,json}] indent_size = 2 ================================================ FILE: .gitignore ================================================ .idea package-lock.json node_modules public/dist .DS_store ================================================ FILE: .travis.yml ================================================ language: node_js node_js: - 10.15.3 addons: ssh_known_hosts: 52.76.67.104 before_deploy: - openssl aes-256-cbc -K $encrypted_7892896c2e12_key -iv $encrypted_7892896c2e12_iv -in light-sail-cuckoo-plus.pem.enc -out light-sail-cuckoo-plus.pem -d - eval "$(ssh-agent -s)" - chmod 600 light-sail-cuckoo-plus.pem - ssh-add light-sail-cuckoo-plus.pem deploy: provider: script script: bash ./deploy.sh skip_cleanup: true branches: only: - master ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2018 Morse_Guo Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Cuckoo.Plus [![Build Status](https://travis-ci.com/NanaMorse/Cuckoo.Plus.svg?branch=master)](https://travis-ci.com/NanaMorse/Cuckoo.Plus) alpha view link: [Cuckoo.Social](http://www.cuckoo.social) Start project. ``` npm i npm run dev ``` then open [localhost:3000](http://localhost:3000) in the browser. Build project. ``` npm run build ``` the files located in public folder are all you need 项目完成状态可以在 [Cuckoo.Social](http://www.cuckoo.social) 中自行体验,没有的就是没做,点不动的一般也是没做。 ## Licence MIT. ================================================ FILE: deploy.sh ================================================ npm run build scp -i "light-sail-cuckoo-plus.pem" -r public ubuntu@52.76.67.104:projects/Cuckoo.Plus/ ================================================ FILE: package.json ================================================ { "name": "cuckoo.plus", "version": "0.3.27", "description": "A third-party client for mastodon", "scripts": { "test": "mocha -r ts-node/register src/**/*.spec.ts", "dev": "webpack-dev-server", "build": "webpack --env=production", "start": "node server.js" }, "dependencies": { "autosize": "^4.0.2", "crypto-js": "^3.1.9-1", "express": "^4.16.4", "file-saver": "^2.0.1", "jquery": "^3.4.1", "less": "^3.9.0", "masonry-layout": "^4.2.2", "moment": "^2.22.2", "muse-ui": "^3.0.1", "muse-ui-loading": "^0.2.0", "muse-ui-message": "^0.2.1", "muse-ui-progress": "^0.1.0", "muse-ui-toast": "^0.3.0", "resize-observer-polyfill": "^1.5.1", "sha1": "^1.1.1", "textarea-caret": "^3.1.0", "underscore": "^1.9.1", "vue": "^2.5.13", "vue-color": "^2.7.0", "vue-i18n": "^8.1.0", "vue-resource": "^1.5.1", "vue-router": "^3.0.1", "vuex": "^3.0.1" }, "devDependencies": { "@types/chai": "^4.1.6", "@types/crypto-js": "^3.1.43", "@types/jquery": "^3.3.31", "@types/less": "^3.0.0", "@types/mocha": "^5.2.5", "@types/node": "^10.11.7", "@types/sha1": "^1.1.1", "@types/underscore": "^1.8.9", "@types/vue-color": "^2.4.2", "@types/vue-resource": "^0.9.34", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-minify-webpack-plugin": "^0.3.1", "babel-plugin-transform-class-properties": "^6.24.1", "babel-preset-es2015": "^6.24.1", "chai": "^4.2.0", "css-loader": "^0.28.9", "husky": "^1.1.2", "less": "^3.8.1", "less-loader": "^4.1.0", "mocha": "^5.2.0", "node-sass": "^4.9.3", "raw-loader": "^0.5.1", "sass-loader": "^7.1.0", "style-loader": "^0.23.1", "ts-loader": "^3.5.0", "ts-node": "^7.0.1", "typescript": "^2.7.1", "vue-loader": "^14.2.3", "vue-property-decorator": "^7.2.0", "vue-template-compiler": "^2.5.13", "vuex-class": "^0.3.1", "webpack": "^3.11.0", "webpack-bundle-analyzer": "^3.0.3", "webpack-dev-server": "^2.11.3", "yargs": "^12.0.2" }, "babel": { "presets": [ "es2015" ] }, "husky": { "hooks": { "pre-push": "npm run test" } }, "repository": { "type": "git", "url": "git+https://github.com/NanaMorse/Cuckoo.Plus.git" }, "author": "Nana.Morse, KTachibanaM, roytam1, cutls, hakaba-hitoyo", "thanks": "roytam1, fhoshino", "license": "MIT", "bugs": { "url": "https://github.com/NanaMorse/Cuckoo.Plus/issues" }, "homepage": "https://github.com/NanaMorse/Cuckoo.Plus#readme" } ================================================ FILE: public/index.html ================================================ Cuckoo+
================================================ FILE: public/manifest.json ================================================ { "name" : "Cuckoo Social", "short_name" : "Cuckoo+", "start_url" : "/", "display" : "standalone", "icons": [ { "src" : "favicon/google_plus/48x48.png", "sizes" : "48x48", "type" : "image/png" }, { "src" : "favicon/google_plus/72x72.png", "sizes" : "72x72", "type" : "image/png" }, { "src" : "favicon/google_plus/96x96.png", "sizes" : "96x96", "type" : "image/png" }, { "src" : "favicon/google_plus/144x144.png", "sizes" : "144x144", "type" : "image/png" }, { "src" : "favicon/google_plus/192x192.png", "sizes" : "192x192", "type" : "image/png" } ] } ================================================ FILE: public/sw.js ================================================ const version = '0.3.27' const CACHE = version + ':CP' const cacheFilePaths = [ '/', '/manifest.json', '/dist/bundle.js', 'https://fonts.loli.net/css?family=Open+Sans', 'https://fonts.loli.net/icon?family=Material+Icons', 'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/moment.min.js', 'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-cn.js', 'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-hk.js', 'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/zh-tw.js', 'https://cdnjs.loli.net/ajax/libs/moment.js/2.22.2/locale/ja.js', 'https://cdnjs.loli.net/ajax/libs/underscore.js/1.9.1/underscore-min.js', 'https://unpkg.com/muse-ui/dist/muse-ui.css', 'https://gstatic.loli.net/s/materialicons/v46/flUhRq6tzZclQEJ-Vdg-IuiaDsNc.woff2', ] const swContext = this class SW { constructor () { this.initInstallEventListener() this.initActivateEventListener() this.initFetchEventListener() } initInstallEventListener () { swContext.addEventListener('install', event => { event.waitUntil(this.installFiles().then(() => swContext.skipWaiting())) }) } installFiles () { return caches.open(CACHE).then(cache => { return cache.addAll(cacheFilePaths) }) } initActivateEventListener () { swContext.addEventListener('activate', event => { // delete old caches event.waitUntil(this.clearOldCaches().then(() => swContext.clients.claim())) }); } clearOldCaches () { return caches.keys().then(keylist => { return Promise.all(keylist.filter(key => key !== CACHE).map(key => caches.delete(key))) }) } initFetchEventListener () { swContext.addEventListener('fetch', event => { // abandon non-GET requests if (event.request.method !== 'GET') return const request = event.request const url = request.url const isRequestImage = event.request.destination === 'image' if (!this.isCacheFilePath(url) && !isRequestImage) return return event.respondWith(caches.open(CACHE).then(cache => { return cache.match(request).then(response => { if (response) return response return fetch(request).then(response => { if (response.ok) cache.put(request, response.clone()) return response }).catch() }) })) }) } isCacheFilePath (url) { return cacheFilePaths.some(filePath => url.endsWith(filePath)) } } new SW() ================================================ FILE: server.js ================================================ const express = require('express') const http = require('http') const https = require('https') const fs = require('fs') const httpApp = express() const httpsApp = express() const httpPort = 80 const httpsPort = 443 httpApp.all("*", (req, res) => { let host = req.headers.host host = host.replace(/:\d+$/, '') res.redirect(301, `https://${host}${req.path}`) }) http.createServer(httpApp).listen(httpPort) httpsApp.use(express.static('public')) https.createServer({ key: fs.readFileSync('../ssl/private.key'), cert: fs.readFileSync('../ssl/certificate.pem') }, httpsApp).listen(httpsPort) ================================================ FILE: src/App.vue ================================================ ================================================ FILE: src/api/accounts.ts ================================================ import Vue from 'vue' import { mastodonentities } from '@/interface' import { patchApiUri } from '@/util' interface updateAccountFormData { // The name to display in the user's profile display_name?: string // A new biography for the user note?: string // An avatar for the user (encoded using multipart/form-data) avatar?: FormData // A header image for the user (encoded using multipart/form-data) header?: FormData // Manually approve followers? locked?: boolean // (2.4 or later) extra source attribute from verify_credentials source?: { privacy?: string sensitive?: boolean note?: string fields?: Array } } async function fetchAccountInfoById () { } async function fetchCurrentUserAccountInfo (): Promise<{ data: mastodonentities.AuthenticatedAccount }> { return Vue.http.get(patchApiUri('/api/v1/accounts/verify_credentials')) as any } async function updateUserAccountInfo (formData: updateAccountFormData): Promise<{ data: mastodonentities.AuthenticatedAccount }> { return Vue.http.patch(patchApiUri('/api/v1/accounts/update_credentials'), formData) as any } async function fetchRelationships (idList: Array) { return Vue.http.get(patchApiUri('/api/v1/accounts/relationships'), { params: { id: idList } }) as any } async function followAccountById (id: string) { return Vue.http.post(patchApiUri(`/api/v1/accounts/${id}/follow`)) as any } async function unFollowAccountById (id: string) { return Vue.http.post(patchApiUri(`/api/v1/accounts/${id}/unfollow`)) as any } export { fetchAccountInfoById, fetchCurrentUserAccountInfo, updateUserAccountInfo, fetchRelationships, followAccountById, unFollowAccountById } ================================================ FILE: src/api/apps.ts ================================================ import Vue from 'vue' import { patchApiUri } from '@/util' const clientName = 'Cuckoo.Plus' const scopes = 'read write follow' namespace Apps { export interface registerApplicationFormData { // Name of your application client_name?: string // Where the user should be redirected after authorization // (for no redirect, use urn:ietf:wg:oauth:2.0:oob) redirect_uris?: string // This can be a space-separated list of the following items: "read", "write" and "follow" scopes?: string // URL to the homepage of your app website?: string } export interface registerApplicationReturnData { data: { client_id: string, client_secret: string, id: string, name: string, redirect_uri: string, website: string | null } } } async function registerApplication (): Promise { const formData: Apps.registerApplicationFormData = { client_name: clientName, redirect_uris: location.origin, scopes: scopes } return Vue.http.post(patchApiUri('/api/v1/apps'), formData) as any } export { registerApplication } ================================================ FILE: src/api/index.ts ================================================ import * as apps from './apps' import * as oauth from './oauth' import * as accounts from './accounts' import * as lists from './lists' import * as timelines from './timelines' import * as statuses from './statuses' import * as notifications from './notifications' import * as media from './media' import * as instances from './instances' import * as search from './search' import streaming from './streaming' export { apps, oauth, accounts, lists, timelines, statuses, notifications, media, streaming, instances, search } ================================================ FILE: src/api/instances.ts ================================================ import Vue from 'vue' import { mastodonentities } from '@/interface' import { patchApiUri } from '@/util' async function getCustomEmojis (): Promise<{ data: Array }> { return Vue.http.get(patchApiUri('/api/v1/custom_emojis')) as any } export { getCustomEmojis } ================================================ FILE: src/api/lists.ts ================================================ import Vue from 'vue' import { patchApiUri } from '@/util' async function receiveLists () { return Vue.http.get(patchApiUri('/api/v1/instance')) } export { receiveLists } ================================================ FILE: src/api/media.ts ================================================ import Vue from 'vue' import { mastodonentities } from '@/interface' import { patchApiUri } from '@/util' async function postMediaFile (formData): Promise<{ data: mastodonentities.Attachment }> { return Vue.http.post(patchApiUri('/api/v1/media'), formData) as any } export { postMediaFile } ================================================ FILE: src/api/notifications.ts ================================================ import Vue from 'vue' import { mastodonentities } from '@/interface' import { patchApiUri } from '@/util' interface getNotificationsQueryParams { // Get a list of notifications with ID less than this value max_id?: string // Get a list of notifications with ID greater than this value since_id?: string // Maximum number of notifications to get (Default 15, Max 30) limit?: number // Array of notifications to exclude (Allowed values: "follow", "favourite", "reblog", "mention") exclude_types?: Array } async function getNotifications(queryParams: getNotificationsQueryParams): Promise<{ data: Array }> { queryParams.limit = 30 const config = { params: queryParams } return Vue.http.get(patchApiUri('/api/v1/notifications'), config) as any } export { getNotifications } ================================================ FILE: src/api/oauth.ts ================================================ import Vue from 'vue' import store from '@/store' import { patchApiUri } from '@/util' import HttpResponse = vuejs.HttpResponse; interface fetchOAuthTokenReturnData extends HttpResponse { data: { access_token: string } } async function fetchOAuthToken (): Promise { const OAuthInfo = store.state.OAuthInfo const formData = { client_id: OAuthInfo.clientId, client_secret: OAuthInfo.clientSecret, redirect_uri: location.origin, grant_type: "authorization_code", code: OAuthInfo.code } return await Vue.http.post(patchApiUri('/oauth/token'), formData) as any } export { fetchOAuthToken } ================================================ FILE: src/api/search.ts ================================================ import Vue from 'vue' import { mastodonentities } from '@/interface' import { patchApiUri } from '@/util' let preSearchRequest /** * @param q The search query * @param resolve Whether to resolve non-local accounts (default: don't resolve) * */ async function getSearchResults (q: string, resolve: boolean = false): Promise<{ data: mastodonentities.SearchResults }> { return Vue.http.get(patchApiUri('/api/v1/search'), { params: { q, resolve }, before(request) { abortSearch() preSearchRequest = request } }) as any } function abortSearch () { if (preSearchRequest) preSearchRequest.abort() } export { getSearchResults, abortSearch } ================================================ FILE: src/api/statuses.ts ================================================ import Vue from 'vue' import { mastodonentities } from '@/interface' import { patchApiUri, generateUniqueKey } from '@/util' import { VisibilityTypes } from '@/constant' async function getStatusById (id: string): Promise<{ data: mastodonentities.Status }> { return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}`)) as any } interface postStatusFormData { // The text of the status status: string // local ID of the status you want to reply to inReplyToId?: string // Array of media IDs to attach to the status (maximum 4) mediaIds?: Array // Set this to mark the media of the status as NSFW sensitive?: boolean // Text to be shown as a warning before the actual content spoilerText?: string // Either "direct", "private", "unlisted" or "public" visibility?: string // ISO 639-2 language code of the toot, to skip automatic detection language?: string } async function postStatus (formData: postStatusFormData): Promise<{ data: mastodonentities.Status }> { const apiFormData: any = {} apiFormData.status = formData.status apiFormData.in_reply_to_id = formData.inReplyToId apiFormData.media_ids = formData.mediaIds apiFormData.sensitive = formData.sensitive apiFormData.spoiler_text = formData.spoilerText apiFormData.visibility = formData.visibility || VisibilityTypes.PUBLIC apiFormData.language = formData.language const config = { headers: { 'Idempotency-Key': generateUniqueKey() } } return Vue.http.post(patchApiUri(`/api/v1/statuses`), apiFormData, config) as any } async function getStatusContextById (id: string): Promise<{ data: mastodonentities.Context }> { return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/context`)) as any } async function getReBloggedAccountsById (id: string): Promise<{ data: Array }> { return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/reblogged_by`)) as any } async function getFavouritedAccountsById (id: string): Promise<{ data: Array }> { return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/favourited_by`)) as any } async function favouriteStatusById (id: string): Promise<{ data: mastodonentities.Status }> { return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/favourite`)) as any } async function unFavouriteStatusById (id: string): Promise<{ data: mastodonentities.Status }> { return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/unfavourite`)) as any } async function reblogStatusById (id: string): Promise<{ data: mastodonentities.Status }> { return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/reblog`)) as any } async function unReblogStatusById (id: string): Promise<{ data: mastodonentities.Status }> { return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/unreblog`)) as any } async function deleteStatusById (id: string) { return Vue.http.delete(patchApiUri(`/api/v1/statuses/${id}`)) as any } async function muteStatusById (id: string) { return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/mute`)) as any } async function unMuteStatusById (id: string) { return Vue.http.post(patchApiUri(`/api/v1/statuses/${id}/unmute`)) as any } async function getStatusCardInfoById (id: string): Promise<{ data: mastodonentities.Card }> { return Vue.http.get(patchApiUri(`/api/v1/statuses/${id}/card`)) as any } export { getStatusById, postStatus, getStatusContextById, getReBloggedAccountsById, getFavouritedAccountsById, favouriteStatusById, unFavouriteStatusById, reblogStatusById, unReblogStatusById, deleteStatusById, muteStatusById, unMuteStatusById, getStatusCardInfoById } ================================================ FILE: src/api/streaming.ts ================================================ import store from '@/store' import { StreamingEventTypes, TimeLineTypes, NotificationTypes, RoutersInfo, I18nTags } from '@/constant' import { mastodonentities } from "@/interface" import router from '@/router' import { extractText, prepareRootStatus } from "@/util" import i18n from '@/i18n' class NotificationHandler { public emit (newNotification: mastodonentities.Notification) { switch (newNotification.type) { case NotificationTypes.MENTION : { return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.mentioned_you)) } case NotificationTypes.REBLOG : { return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.boosted_your_status)) } case NotificationTypes.FAVOURITE : { // update status info store.dispatch('fetchStatusById', newNotification.status.id) return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.favourited_your_status)) } case NotificationTypes.FOLLOW : { store.dispatch('updateRelationships', { idList: [newNotification.account.id] }) return this.emitStatusOperateNotification(newNotification, i18n.t(I18nTags.notifications.someone_followed_you)) } } } private emitStatusOperateNotification (newNotification: mastodonentities.Notification, operateTypeString) { const title = `${this.getFromName(newNotification)} ${operateTypeString}` const bodyText = newNotification.status ? extractText(newNotification.status.content) : '' // ignore all muted status's notification if (newNotification.status && (store.state.appStatus.settings.muteMap.statusList.indexOf(newNotification.status) !== -1)) return if (store.state.appStatus.settings.muteMap.userList.indexOf(newNotification.account.id) !== -1) return const nativeNotification = new Notification(title, { body: bodyText, icon: this.getImageUrl(newNotification) }) nativeNotification.addEventListener('click', () => { if (store.state.appStatus.unreadNotificationCount > 0) { store.commit('updateUnreadNotificationCount', store.state.appStatus.unreadNotificationCount - 1) } this.routeToTargetStatus(newNotification) }) } private getFromName (newNotification: mastodonentities.Notification): string { // account's display name have been formatted return store.getters['getAccountDisplayName'](newNotification.account) .replace('', '').replace('', '') } private getImageUrl (newNotification: mastodonentities.Notification): string { return newNotification.account.avatar } private async routeToTargetStatus (newNotification: mastodonentities.Notification) { const targetStatus = await prepareRootStatus(newNotification.status) router.push({ name: RoutersInfo.statuses.name, params: { statusId: targetStatus.id } }) } private routeToTargetAccount () { } } const notificationHandler = new NotificationHandler() class Streaming { private userStreamWs: WebSocket private localStreamWs: WebSocket private publicStreamWs: WebSocket private createWsUrl (streamName: string) { return `wss://${new URL(store.state.mastodonServerUri).hostname}/api/v1/streaming/?stream=${streamName}&access_token=${store.state.OAuthInfo.accessToken}` } public openUserConnection () { const wsUrl = this.createWsUrl('user') this.userStreamWs = new WebSocket(wsUrl) this.initEventListener(this.userStreamWs, TimeLineTypes.HOME) } public openLocalConnection () { const wsUrl = this.createWsUrl('public:local') this.localStreamWs = new WebSocket(wsUrl) this.initEventListener(this.localStreamWs, TimeLineTypes.LOCAL) } public openPublicConnection () { const wsUrl = this.createWsUrl('public') this.publicStreamWs = new WebSocket(wsUrl) this.initEventListener(this.publicStreamWs, TimeLineTypes.PUBLIC) } public closeConnection (timeLineType: string) { const typeToWsMap = { [TimeLineTypes.HOME]: this.userStreamWs, [TimeLineTypes.LOCAL]: this.localStreamWs, [TimeLineTypes.PUBLIC]: this.publicStreamWs } typeToWsMap[timeLineType].close() } private initEventListener (targetWs: WebSocket, timeLineType, hashName?) { targetWs.onmessage = (message) => { if(message.data.length) { const parsedMessage = JSON.parse(message.data) switch (parsedMessage.event) { case StreamingEventTypes.UPDATE : { return this.updateStatus(JSON.parse(parsedMessage.payload), timeLineType, hashName) } case StreamingEventTypes.DELETE : { return this.deleteStatus(parsedMessage.payload) } case StreamingEventTypes.NOTIFICATION : { return this.emitNotification(JSON.parse(parsedMessage.payload)) } } } } } private updateStatus (newStatus: mastodonentities.Status, timeLineType, hashName?) { if (store.state.statusMap[newStatus.id]) return // update status map store.commit('updateStatusMap', { [newStatus.id]: newStatus }) store.dispatch('updateCardMap', newStatus.id) if (timeLineType === TimeLineTypes.HOME) { prepareRootStatus(newStatus) } // update target timeline list const targetMutationName = store.state.appStatus.settings.realTimeLoadStatusMode ? 'unShiftTimeLineStatuses' : 'unShiftStreamStatusesPool' store.commit(targetMutationName, { newStatusIdList: [newStatus.id], timeLineType, hashName }) } private deleteStatus (statusId: string) { if (!store.state.statusMap[statusId]) return // remove from time line store.commit('deleteStatusFromTimeLine', statusId) // remove from status map store.commit('removeStatusFromStatusMapById', statusId) } private emitNotification (newNotification: mastodonentities.Notification) { // update notification list store.commit('unShiftNotification', [newNotification]) // set notification icon unread store.commit('updateUnreadNotificationCount', store.state.appStatus.unreadNotificationCount + 1) // send browser notification // @ts-ignore if (window.Notification) { notificationHandler.emit(newNotification) } } } export default new Streaming() ================================================ FILE: src/api/timelines.ts ================================================ import Vue from 'vue' import { patchApiUri, isBaseTimeLine } from '@/util' import { TimeLineTypes } from '@/constant' import { mastodonentities } from '@/interface' const allTimeLineTypeList = [ TimeLineTypes.HOME, TimeLineTypes.PUBLIC, TimeLineTypes.DIRECT, TimeLineTypes.LOCAL, TimeLineTypes.TAG, TimeLineTypes.LIST ] async function getTimeLineStatuses ({ timeLineType = '', maxId = '', sinceId = '', hashName = '', limit = 40, local = false} = {}): Promise<{ data: Array }> { if (allTimeLineTypeList.indexOf(timeLineType) === -1) throw new Error('unknown timeline type!') let urlFragmentString = '' if (isBaseTimeLine(timeLineType)) { urlFragmentString = timeLineType } else { if (!hashName) throw new Error('need a hash name!') urlFragmentString = `${timeLineType}/${hashName}` } const params: any = { limit: limit } if (maxId) params.max_id = maxId if (sinceId) params.since_id = sinceId if (local) params.local = true if (timeLineType === TimeLineTypes.LOCAL) { urlFragmentString = TimeLineTypes.PUBLIC params.local = true } return Vue.http.get(patchApiUri(`/api/v1/timelines/${urlFragmentString}`), { params }) as any } export { getTimeLineStatuses } ================================================ FILE: src/components/Drawer/PeopleResultCard.vue ================================================ ================================================ FILE: src/components/Drawer/Search.vue ================================================ ================================================ FILE: src/components/Drawer/index.vue ================================================ ================================================ FILE: src/components/Header.vue ================================================ ================================================ FILE: src/components/Input.vue ================================================