diff options
| author | Henry Jameson <me@hjkos.com> | 2018-11-26 04:38:44 +0300 |
|---|---|---|
| committer | Henry Jameson <me@hjkos.com> | 2018-11-26 04:38:44 +0300 |
| commit | e06717fd0dfa4b37ebf481d5f4cd7ce8ef0034d0 (patch) | |
| tree | 7f3f7d27ac932e72ec9b5fbdb41b7c8efb210396 /src | |
| parent | 0ca42bd3d63e209f9c1354a30a3123c1f7317579 (diff) | |
| parent | f1a23f2b6edb0858890c82cf42c8b6d835102d56 (diff) | |
Merge remote-tracking branch 'upstream/develop' into feature/scope_preferences
* upstream/develop:
DM timeline: stream new statuses
update-japanese-translation
Add actual user search.
incorporate most translation changes from MR 368
update french translation
Always show dm panel.
Add direct message tab.
api service url
On logout switch to public timeline.
Put oauth text into description.
Display OAuth login on login form button.
Add login form back in.
Linting.
Re-activate registration, use oauth password flow to fetch token.
Fix typo.
Remove gonsole.logg :DD
Fix linting.
Move login to oauth.
Diffstat (limited to 'src')
28 files changed, 587 insertions, 274 deletions
@@ -73,6 +73,7 @@ export default { window.scrollTo(0, 0) }, logout () { + this.$router.replace('/main/public') this.$store.dispatch('logout') } } diff --git a/src/boot/after_store.js b/src/boot/after_store.js new file mode 100644 index 00000000..ea5d4ecd --- /dev/null +++ b/src/boot/after_store.js @@ -0,0 +1,184 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' + +import App from '../App.vue' +import PublicTimeline from '../components/public_timeline/public_timeline.vue' +import PublicAndExternalTimeline from '../components/public_and_external_timeline/public_and_external_timeline.vue' +import FriendsTimeline from '../components/friends_timeline/friends_timeline.vue' +import TagTimeline from '../components/tag_timeline/tag_timeline.vue' +import ConversationPage from '../components/conversation-page/conversation-page.vue' +import Mentions from '../components/mentions/mentions.vue' +import DMs from '../components/dm_timeline/dm_timeline.vue' +import UserProfile from '../components/user_profile/user_profile.vue' +import Settings from '../components/settings/settings.vue' +import Registration from '../components/registration/registration.vue' +import UserSettings from '../components/user_settings/user_settings.vue' +import FollowRequests from '../components/follow_requests/follow_requests.vue' +import OAuthCallback from '../components/oauth_callback/oauth_callback.vue' +import UserSearch from '../components/user_search/user_search.vue' + +const afterStoreSetup = ({store, i18n}) => { + window.fetch('/api/statusnet/config.json') + .then((res) => res.json()) + .then((data) => { + const {name, closed: registrationClosed, textlimit, server} = data.site + + store.dispatch('setInstanceOption', { name: 'name', value: name }) + store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) + store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) + store.dispatch('setInstanceOption', { name: 'server', value: server }) + + var apiConfig = data.site.pleromafe + + window.fetch('/static/config.json') + .then((res) => res.json()) + .catch((err) => { + console.warn('Failed to load static/config.json, continuing without it.') + console.warn(err) + return {} + }) + .then((staticConfig) => { + // This takes static config and overrides properties that are present in apiConfig + var config = Object.assign({}, staticConfig, apiConfig) + + var theme = (config.theme) + var background = (config.background) + var hidePostStats = (config.hidePostStats) + var hideUserStats = (config.hideUserStats) + var logo = (config.logo) + var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask) + var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin) + var redirectRootNoLogin = (config.redirectRootNoLogin) + var redirectRootLogin = (config.redirectRootLogin) + var chatDisabled = (config.chatDisabled) + var showInstanceSpecificPanel = (config.showInstanceSpecificPanel) + var scopeOptionsEnabled = (config.scopeOptionsEnabled) + var formattingOptionsEnabled = (config.formattingOptionsEnabled) + var collapseMessageWithSubject = (config.collapseMessageWithSubject) + var loginMethod = (config.loginMethod) + var scopeCopy = (config.scopeCopy) + var subjectLineBehavior = (config.subjectLineBehavior) + + store.dispatch('setInstanceOption', { name: 'theme', value: theme }) + store.dispatch('setInstanceOption', { name: 'background', value: background }) + store.dispatch('setInstanceOption', { name: 'hidePostStats', value: hidePostStats }) + store.dispatch('setInstanceOption', { name: 'hideUserStats', value: hideUserStats }) + store.dispatch('setInstanceOption', { name: 'logo', value: logo }) + store.dispatch('setInstanceOption', { name: 'logoMask', value: logoMask }) + store.dispatch('setInstanceOption', { name: 'logoMargin', value: logoMargin }) + store.dispatch('setInstanceOption', { name: 'redirectRootNoLogin', value: redirectRootNoLogin }) + store.dispatch('setInstanceOption', { name: 'redirectRootLogin', value: redirectRootLogin }) + store.dispatch('setInstanceOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel }) + store.dispatch('setInstanceOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled }) + store.dispatch('setInstanceOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled }) + store.dispatch('setInstanceOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject }) + store.dispatch('setInstanceOption', { name: 'loginMethod', value: loginMethod }) + store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy }) + store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior }) + if (chatDisabled) { + store.dispatch('disableChat') + } + + const routes = [ + { name: 'root', + path: '/', + redirect: to => { + return (store.state.users.currentUser + ? store.state.instance.redirectRootLogin + : store.state.instance.redirectRootNoLogin) || '/main/all' + }}, + { path: '/main/all', component: PublicAndExternalTimeline }, + { path: '/main/public', component: PublicTimeline }, + { path: '/main/friends', component: FriendsTimeline }, + { path: '/tag/:tag', component: TagTimeline }, + { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, + { name: 'user-profile', path: '/users/:id', component: UserProfile }, + { name: 'mentions', path: '/:username/mentions', component: Mentions }, + { name: 'dms', path: '/:username/dms', component: DMs }, + { name: 'settings', path: '/settings', component: Settings }, + { name: 'registration', path: '/registration', component: Registration }, + { name: 'registration', path: '/registration/:token', component: Registration }, + { name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, + { name: 'user-settings', path: '/user-settings', component: UserSettings }, + { name: 'oauth-callback', path: '/oauth-callback', component: OAuthCallback, props: (route) => ({ code: route.query.code }) }, + { name: 'user-search', path: '/user-search', component: UserSearch, props: (route) => ({ query: route.query.query }) } + ] + + const router = new VueRouter({ + mode: 'history', + routes, + scrollBehavior: (to, from, savedPosition) => { + if (to.matched.some(m => m.meta.dontScroll)) { + return false + } + return savedPosition || { x: 0, y: 0 } + } + }) + + /* eslint-disable no-new */ + new Vue({ + router, + store, + i18n, + el: '#app', + render: h => h(App) + }) + }) + }) + + window.fetch('/static/terms-of-service.html') + .then((res) => res.text()) + .then((html) => { + store.dispatch('setInstanceOption', { name: 'tos', value: html }) + }) + + window.fetch('/api/pleroma/emoji.json') + .then( + (res) => res.json() + .then( + (values) => { + const emoji = Object.keys(values).map((key) => { + return { shortcode: key, image_url: values[key] } + }) + store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) + }, + (failure) => { + store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false }) + } + ), + (error) => console.log(error) + ) + + window.fetch('/static/emoji.json') + .then((res) => res.json()) + .then((values) => { + const emoji = Object.keys(values).map((key) => { + return { shortcode: key, image_url: false, 'utf': values[key] } + }) + store.dispatch('setInstanceOption', { name: 'emoji', value: emoji }) + }) + + window.fetch('/instance/panel.html') + .then((res) => res.text()) + .then((html) => { + store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) + }) + + window.fetch('/nodeinfo/2.0.json') + .then((res) => res.json()) + .then((data) => { + const metadata = data.metadata + + const features = metadata.features + store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) + store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) + store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) + + const suggestions = metadata.suggestions + store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) + store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) + }) +} + +export default afterStoreSetup diff --git a/src/components/dm_timeline/dm_timeline.js b/src/components/dm_timeline/dm_timeline.js new file mode 100644 index 00000000..8b5393a9 --- /dev/null +++ b/src/components/dm_timeline/dm_timeline.js @@ -0,0 +1,14 @@ +import Timeline from '../timeline/timeline.vue' + +const DMs = { + computed: { + timeline () { + return this.$store.state.statuses.timelines.dms + } + }, + components: { + Timeline + } +} + +export default DMs diff --git a/src/components/dm_timeline/dm_timeline.vue b/src/components/dm_timeline/dm_timeline.vue new file mode 100644 index 00000000..f03da4d3 --- /dev/null +++ b/src/components/dm_timeline/dm_timeline.vue @@ -0,0 +1,5 @@ +<template> + <Timeline :title="$t('nav.dms')" v-bind:timeline="timeline" v-bind:timeline-name="'dms'"/> +</template> + +<script src="./dm_timeline.js"></script> diff --git a/src/components/login_form/login_form.js b/src/components/login_form/login_form.js index 4405fb92..49868aed 100644 --- a/src/components/login_form/login_form.js +++ b/src/components/login_form/login_form.js @@ -1,22 +1,40 @@ +import oauthApi from '../../services/new_api/oauth.js' const LoginForm = { data: () => ({ user: {}, authError: false }), computed: { + loginMethod () { return this.$store.state.instance.loginMethod }, loggingIn () { return this.$store.state.users.loggingIn }, registrationOpen () { return this.$store.state.instance.registrationOpen } }, methods: { + oAuthLogin () { + oauthApi.login({ + oauth: this.$store.state.oauth, + instance: this.$store.state.instance.server, + commit: this.$store.commit + }) + }, submit () { - this.$store.dispatch('loginUser', this.user).then( - () => {}, - (error) => { - this.authError = error - this.user.username = '' - this.user.password = '' - } - ) + const data = { + oauth: this.$store.state.oauth, + instance: this.$store.state.instance.server + } + oauthApi.getOrCreateApp(data).then((app) => { + oauthApi.getTokenWithCredentials( + { + app, + instance: data.instance, + username: this.user.username, + password: this.user.password}) + .then((result) => { + this.$store.commit('setToken', result.access_token) + this.$store.dispatch('loginUser', result.access_token) + this.$router.push('/main/friends') + }) + }) } } } diff --git a/src/components/login_form/login_form.vue b/src/components/login_form/login_form.vue index b7fed48a..12971882 100644 --- a/src/components/login_form/login_form.vue +++ b/src/components/login_form/login_form.vue @@ -5,7 +5,7 @@ {{$t('login.login')}} </div> <div class="panel-body"> - <form v-on:submit.prevent='submit(user)' class='login-form'> + <form v-if="loginMethod == 'password'" v-on:submit.prevent='submit(user)' class='login-form'> <div class='form-group'> <label for='username'>{{$t('login.username')}}</label> <input :disabled="loggingIn" v-model='user.username' class='form-control' id='username' v-bind:placeholder="$t('login.placeholder')"> @@ -20,8 +20,17 @@ <button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button> </div> </div> - <div v-if="authError" class='form-group'> - <div class='alert error'>{{authError}}</div> + </form> + + <form v-if="loginMethod == 'token'" v-on:submit.prevent='oAuthLogin' class="login-form"> + <div class="form-group"> + <p>{{$t('login.description')}}</p> + </div> + <div class='form-group'> + <div class='login-bottom'> + <div><router-link :to="{name: 'registration'}" v-if='registrationOpen' class='register'>{{$t('login.register')}}</router-link></div> + <button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button> + </div> </div> </form> </div> diff --git a/src/components/nav_panel/nav_panel.vue b/src/components/nav_panel/nav_panel.vue index 0b188f9a..93deaf97 100644 --- a/src/components/nav_panel/nav_panel.vue +++ b/src/components/nav_panel/nav_panel.vue @@ -12,6 +12,11 @@ {{ $t("nav.mentions") }} </router-link> </li> + <li v-if='currentUser'> + <router-link :to="{ name: 'dms', params: { username: currentUser.screen_name } }"> + {{ $t("nav.dms") }} + </router-link> + </li> <li v-if='currentUser && currentUser.locked'> <router-link to='/friend-requests'> {{ $t("nav.friend_requests") }} diff --git a/src/components/oauth_callback/oauth_callback.js b/src/components/oauth_callback/oauth_callback.js new file mode 100644 index 00000000..7a5132ad --- /dev/null +++ b/src/components/oauth_callback/oauth_callback.js @@ -0,0 +1,20 @@ +import oauth from '../../services/new_api/oauth.js' + +const oac = { + props: ['code'], + mounted () { + if (this.code) { + oauth.getToken({ + app: this.$store.state.oauth, + instance: this.$store.state.instance.server, + code: this.code + }).then((result) => { + this.$store.commit('setToken', result.access_token) + this.$store.dispatch('loginUser', result.access_token) + this.$router.push('/main/friends') + }) + } + } +} + +export default oac diff --git a/src/components/oauth_callback/oauth_callback.vue b/src/components/oauth_callback/oauth_callback.vue new file mode 100644 index 00000000..9c806916 --- /dev/null +++ b/src/components/oauth_callback/oauth_callback.vue @@ -0,0 +1,5 @@ +<template> + <h1>...</h1> +</template> + +<script src="./oauth_callback.js"></script> diff --git a/src/components/registration/registration.js b/src/components/registration/registration.js index 8f59878d..f7f8a720 100644 --- a/src/components/registration/registration.js +++ b/src/components/registration/registration.js @@ -1,3 +1,5 @@ +import oauthApi from '../../services/new_api/oauth.js' + const registration = { data: () => ({ user: {}, @@ -25,9 +27,23 @@ const registration = { this.$store.state.api.backendInteractor.register(this.user).then( (response) => { if (response.ok) { - this.$store.dispatch('loginUser', this.user) - this.$router.push('/main/all') - this.registering = false + const data = { + oauth: this.$store.state.oauth, + instance: this.$store.state.instance.server + } + oauthApi.getOrCreateApp(data).then((app) => { + oauthApi.getTokenWithCredentials( + { + app, + instance: data.instance, + username: this.user.username, + password: this.user.password}) + .then((result) => { + this.$store.commit('setToken', result.access_token) + this.$store.dispatch('loginUser', result.access_token) + this.$router.push('/main/friends') + }) + }) } else { this.registering = false response.json().then((data) => { diff --git a/src/components/user_finder/user_finder.js b/src/components/user_finder/user_finder.js index a743b5f6..74f79d1b 100644 --- a/src/components/user_finder/user_finder.js +++ b/src/components/user_finder/user_finder.js @@ -7,25 +7,10 @@ const UserFinder = { }), methods: { findUser (username) { - username = username[0] === '@' ? username.slice(1) : username - this.loading = true - this.$store.state.api.backendInteractor.externalProfile(username) - .then((user) => { - this.loading = false - this.hidden = true - if (!user.error) { - this.$store.commit('addNewUsers', [user]) - this.$router.push({name: 'user-profile', params: {id: user.id}}) - } else { - this.error = true - } - }) + this.$router.push({ name: 'user-search', query: { query: username } }) }, toggleHidden () { this.hidden = !this.hidden - }, - dismissError () { - this.error = false } } } diff --git a/src/components/user_finder/user_finder.vue b/src/components/user_finder/user_finder.vue index 69bd1d21..f2556569 100644 --- a/src/components/user_finder/user_finder.vue +++ b/src/components/user_finder/user_finder.vue @@ -1,9 +1,5 @@ <template> <span class="user-finder-container"> - <span class="alert error" v-if="error"> - <i class="icon-cancel user-finder-icon" @click="dismissError"/> - {{$t('finder.error_fetching_user')}} - </span> <i class="icon-spin4 user-finder-icon animate-spin-slow" v-if="loading" /> <a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden"/></a> <span v-else> diff --git a/src/components/user_search/user_search.js b/src/components/user_search/user_search.js new file mode 100644 index 00000000..1e488f0c --- /dev/null +++ b/src/components/user_search/user_search.js @@ -0,0 +1,33 @@ +import UserCard from '../user_card/user_card.vue' +import userSearchApi from '../../services/new_api/user_search.js' +const userSearch = { + components: { + UserCard + }, + props: [ + 'query' + ], + data () { + return { + users: [] + } + }, + mounted () { + this.search(this.query) + }, + watch: { + query (newV) { + this.search(newV) + } + }, + methods: { + search (query) { + userSearchApi.search({query, store: this.$store}) + .then((res) => { + this.users = res + }) + } + } +} + +export default userSearch diff --git a/src/components/user_search/user_search.vue b/src/components/user_search/user_search.vue new file mode 100644 index 00000000..1624ebef --- /dev/null +++ b/src/components/user_search/user_search.vue @@ -0,0 +1,12 @@ +<template> + <div class="user-seach panel panel-default"> + <div class="panel-heading"> + {{$t('nav.user_search')}} + </div> + <div class="panel-body"> + <user-card v-for="user in users" :key="user.id" :user="user" :showFollows="true"></user-card> + </div> + </div> +</template> + +<script src="./user_search.js"></script> diff --git a/src/i18n/de.json b/src/i18n/de.json index 73de5949..ff6db67f 100644 --- a/src/i18n/de.json +++ b/src/i18n/de.json @@ -21,6 +21,7 @@ }, "login": { "login": "Anmelden", + "description": "Mit OAuth anmelden", "logout": "Abmelden", "password": "Passwort", "placeholder": "z.B. lain", diff --git a/src/i18n/en.json b/src/i18n/en.json index dafdd11c..314fa083 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -21,6 +21,7 @@ }, "login": { "login": "Log in", + "description": "Log in with OAuth", "logout": "Log out", "password": "Password", "placeholder": "e.g. lain", @@ -31,9 +32,11 @@ "chat": "Local Chat", "friend_requests": "Follow Requests", "mentions": "Mentions", + "dms": "Direct Messages", "public_tl": "Public Timeline", "timeline": "Timeline", - "twkn": "The Whole Known Network" + "twkn": "The Whole Known Network", + "user_search": "User Search" }, "notifications": { "broken_favorite": "Unknown status, searching for it...", diff --git a/src/i18n/fr.json b/src/i18n/fr.json index 3f3550f8..129b7d7c 100644 --- a/src/i18n/fr.json +++ b/src/i18n/fr.json @@ -21,6 +21,7 @@ }, "login": { "login": "Connexion", + "description": "Connexion avec OAuth", "logout": "Déconnexion", "password": "Mot de passe", "placeholder": "p.e. lain", @@ -30,6 +31,7 @@ "nav": { "chat": "Chat local", "friend_requests": "Demandes d'ami", + "dms": "Messages adressés", "mentions": "Notifications", "public_tl": "Statuts locaux", "timeline": "Journal", @@ -86,7 +88,7 @@ "cRed": "Rouge (Annuler)", "change_password": "Changez votre mot de passe", "change_password_error": "Il y a eu un problème pour changer votre mot de passe.", - "changed_password": "Mot de passe changé avec succès!", + "changed_password": "Mot de passe modifié avec succès !", "collapse_subject": "Réduire les messages avec des sujets", "confirm_new_password": "Confirmation du nouveau mot de passe", "current_avatar": "Avatar actuel", @@ -100,17 +102,19 @@ "delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.", "export_theme": "Enregistrer le thème", "filtering": "Filtre", - "filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne.", + "filtering_explanation": "Tous les statuts contenant ces mots seront masqués. Un mot par ligne", "follow_export": "Exporter les abonnements", "follow_export_button": "Exporter les abonnements en csv", "follow_export_processing": "Exportation en cours…", "follow_import": "Importer des abonnements", - "follow_import_error": "Erreur lors de l'importation des abonnements.", + "follow_import_error": "Erreur lors de l'importation des abonnements", "follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.", "foreground": "Premier plan", "general": "Général", "hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations", "hide_attachments_in_tl": "Masquer les pièces jointes dans le journal", + "hide_post_stats": "Masquer les statistiques de publication (le nombre de favoris)", + "hide_user_stats": "Masquer les statistiques de profil (le nombre d'amis)", "import_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv", "import_theme": "Charger le thème", "inputRadius": "Champs de texte", @@ -156,8 +160,8 @@ "streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page", "text": "Texte", "theme": "Thème", - "theme_help": "Spécifiez des codes couleur hexadécimaux (#aabbcc) pour personnaliser les couleurs du thème", - "tooltipRadius": "Info-bulles/alertes ", + "theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.", + "tooltipRadius": "Info-bulles/alertes", "user_settings": "Paramètres utilisateur", "values": { "false": "non", @@ -177,7 +181,7 @@ "user_card": { "approve": "Accepter", "block": "Bloquer", - "blocked": "Bloqué", + "blocked": "Bloqué !", "deny": "Rejeter", "follow": "Suivre", "followees": "Suivis", diff --git a/src/i18n/ja.json b/src/i18n/ja.json index 5a31c58f..4da7ea30 100644 --- a/src/i18n/ja.json +++ b/src/i18n/ja.json @@ -21,6 +21,7 @@ }, "login": { "login": "ログイン", + "description": "OAuthでログイン", "logout": "ログアウト", "password": "パスワード", "placeholder": "れい: lain", @@ -31,6 +32,7 @@ "chat": "ローカルチャット", "friend_requests": "フォローリクエスト", "mentions": "メンション", + "dms": "ダイレクトメッセージ", "public_tl": "パブリックタイムライン", "timeline": "タイムライン", "twkn": "つながっているすべてのネットワーク" @@ -111,6 +113,8 @@ "general": "ぜんぱん", "hide_attachments_in_convo": "スレッドのファイルをかくす", "hide_attachments_in_tl": "タイムラインのファイルをかくす", + "hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)", + "hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)", "import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする", "import_theme": "ロード", "inputRadius": "インプットフィールド", diff --git a/src/lib/persisted_state.js b/src/lib/persisted_state.js index 006107e2..32fc93c6 100644 --- a/src/lib/persisted_state.js +++ b/src/lib/persisted_state.js @@ -17,7 +17,9 @@ const saveImmedeatelyActions = [ 'clearCurrentUser', 'setCurrentUser', 'setHighlight', - 'setOption' + 'setOption', + 'setClientData', + 'setToken' ] const defaultStorage = (() => { @@ -43,8 +45,8 @@ export default function createPersistedState ({ storage = defaultStorage, subscriber = store => handler => store.subscribe(handler) } = {}) { - return store => { - getState(key, storage).then((savedState) => { + return getState(key, storage).then((savedState) => { + return store => { try { if (typeof savedState === 'object') { // build user cache @@ -67,36 +69,35 @@ export default function createPersistedState ({ value: store.state.config.customTheme }) } - if (store.state.users.lastLoginName) { - store.dispatch('loginUser', {username: store.state.users.lastLoginName, password: 'xxx'}) + if (store.state.oauth.token) { + store.dispatch('loginUser', store.state.oauth.token) } loaded = true } catch (e) { console.log("Couldn't load state") loaded = true } - }) - - subscriber(store)((mutation, state) => { - try { - if (saveImmedeatelyActions.includes(mutation.type)) { - setState(key, reducer(state, paths), storage) - .then(success => { - if (typeof success !== 'undefined') { + subscriber(store)((mutation, state) => { + try { + if (saveImmedeatelyActions.includes(mutation.type)) { + setState(key, reducer(state, paths), storage) + .then(success => { + if (typeof success !== 'undefined') { + if (mutation.type === 'setOption') { + store.dispatch('settingsSaved', { success }) + } + } + }, error => { if (mutation.type === 'setOption') { - store.dispatch('settingsSaved', { success }) + store.dispatch('settingsSaved', { error }) } - } - }, error => { - if (mutation.type === 'setOption') { - store.dispatch('settingsSaved', { error }) - } - }) + }) + } + } catch (e) { + console.log("Couldn't persist state:") + console.log(e) } - } catch (e) { - console.log("Couldn't persist state:") - console.log(e) - } - }) - } + }) + } + }) } diff --git a/src/main.js b/src/main.js index 44ca231f..5f459d46 100644 --- a/src/main.js +++ b/src/main.js @@ -1,18 +1,6 @@ import Vue from 'vue' import VueRouter from 'vue-router' import Vuex from 'vuex' -import App from './App.vue' -import PublicTimeline from './components/public_timeline/public_timeline.vue' -import PublicAndExternalTimeline from './components/public_and_external_timeline/public_and_external_timeline.vue' -import FriendsTimeline from './components/friends_timeline/friends_timeline.vue' -import TagTimeline from './components/tag_timeline/tag_timeline.vue' -import ConversationPage from './components/conversation-page/conversation-page.vue' -import Mentions from './components/mentions/mentions.vue' -import UserProfile from './components/user_profile/user_profile.vue' -import Settings from './components/settings/settings.vue' -import Registration from './components/registration/registration.vue' -import UserSettings from './components/user_settings/user_settings.vue' -import FollowRequests from './components/follow_requests/follow_requests.vue' import interfaceModule from './modules/interface.js' import instanceModule from './modules/instance.js' @@ -21,6 +9,7 @@ import usersModule from './modules/users.js' import apiModule from './modules/api.js' import configModule from './modules/config.js' import chatModule from './modules/chat.js' +import oauthModule from './modules/oauth.js' import VueTimeago from 'vue-timeago' import VueI18n from 'vue-i18n' @@ -31,6 +20,8 @@ import messages from './i18n/messages.js' import VueChatScroll from 'vue-chat-scroll' +import afterStoreSetup from './boot/after_store.js' + const currentLocale = (window.navigator.language || 'en').split('-')[0] Vue.use(Vuex) @@ -45,29 +36,6 @@ Vue.use(VueTimeago, { Vue.use(VueI18n) Vue.use(VueChatScroll) -const persistedStateOptions = { - paths: [ - 'config', - 'users.lastLoginName', - 'statuses.notifications.maxSavedId' - ] -} - -const store = new Vuex.Store({ - modules: { - interface: interfaceModule, - instance: instanceModule, - statuses: statusesModule, - users: usersModule, - api: apiModule, - config: configModule, - chat: chatModule - }, - plugins: [createPersistedState(persistedStateOptions)], - strict: false // Socket modifies itself, let's ignore this for now. - // strict: process.env.NODE_ENV !== 'production' -}) - const i18n = new VueI18n({ // By default, use the browser locale, we will update it if neccessary locale: currentLocale, @@ -75,159 +43,32 @@ const i18n = new VueI18n({ messages }) -window.fetch('/api/statusnet/config.json') - .then((res) => res.json()) - .then((data) => { - const {name, closed: registrationClosed, textlimit, server} = data.site - - store.dispatch('setInstanceOption', { name: 'name', value: name }) - store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) - store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) - store.dispatch('setInstanceOption', { name: 'server', value: server }) - - var apiConfig = data.site.pleromafe - - window.fetch('/static/config.json') - .then((res) => res.json()) - .catch((err) => { - console.warn('Failed to load static/config.json, continuing without it.') - console.warn(err) - return {} - }) - .then((staticConfig) => { - // This takes static config and overrides properties that are present in apiConfig - var config = Object.assign({}, staticConfig, apiConfig) - - var theme = (config.theme) - var background = (config.background) - var hidePostStats = (config.hidePostStats) - var hideUserStats = (config.hideUserStats) - var logo = (config.logo) - var logoMask = (typeof config.logoMask === 'undefined' ? true : config.logoMask) - var logoMargin = (typeof config.logoMargin === 'undefined' ? 0 : config.logoMargin) - var redirectRootNoLogin = (config.redirectRootNoLogin) - var redirectRootLogin = (config.redirectRootLogin) - var chatDisabled = (config.chatDisabled) - var showInstanceSpecificPanel = (config.showInstanceSpecificPanel) - var scopeOptionsEnabled = (config.scopeOptionsEnabled) - var formattingOptionsEnabled = (config.formattingOptionsEnabled) - var collapseMessageWithSubject = (config.collapseMessageWithSubject) - var scopeCopy = (config.scopeCopy) - var subjectLineBehavior = (config.subjectLineBehavior) - - store.dispatch('setInstanceOption', { name: 'theme', value: theme }) - store.dispatch('setInstanceOption', { name: 'background', value: background }) - store.dispatch('setInstanceOption', { name: 'hidePostStats', value: hidePostStats }) - store.dispatch('setInstanceOption', { name: 'hideUserStats', value: hideUserStats }) - store.dispatch('setInstanceOption', { name: 'logo', value: logo }) - store.dispatch('setInstanceOption', { name: 'logoMask', value: logoMask }) - store.dispatch('setInstanceOption', { name: 'logoMargin', value: logoMargin }) - store.dispatch('setInstanceOption', { name: 'redirectRootNoLogin', value: redirectRootNoLogin }) - store.dispatch('setInstanceOption', { name: 'redirectRootLogin', value: redirectRootLogin }) - store.dispatch('setInstanceOption', { name: 'showInstanceSpecificPanel', value: showInstanceSpecificPanel }) - store.dispatch('setInstanceOption', { name: 'scopeOptionsEnabled', value: scopeOptionsEnabled }) - store.dispatch('setInstanceOption', { name: 'formattingOptionsEnabled', value: formattingOptionsEnabled }) - store.dispatch('setInstanceOption', { name: 'collapseMessageWithSubject', value: collapseMessageWithSubject }) - store.dispatch('setInstanceOption', { name: 'scopeCopy', value: scopeCopy }) - store.dispatch('setInstanceOption', { name: 'subjectLineBehavior', value: subjectLineBehavior }) - if (chatDisabled) { - store.dispatch('disableChat') - } - - const routes = [ - { name: 'root', - path: '/', - redirect: to => { - return (store.state.users.currentUser - ? store.state.instance.redirectRootLogin - : store.state.instance.redirectRootNoLogin) || '/main/all' - }}, - { path: '/main/all', component: PublicAndExternalTimeline }, - { path: '/main/public', component: PublicTimeline }, - { path: '/main/friends', component: FriendsTimeline }, - { path: '/tag/:tag', component: TagTimeline }, - { name: 'conversation', path: '/notice/:id', component: ConversationPage, meta: { dontScroll: true } }, - { name: 'user-profile', path: '/users/:id', component: UserProfile }, - { name: 'mentions', path: '/:username/mentions', component: Mentions }, - { name: 'settings', path: '/settings', component: Settings }, - { name: 'registration', path: '/registration', component: Registration }, - { name: 'registration', path: '/registration/:token', component: Registration }, - { name: 'friend-requests', path: '/friend-requests', component: FollowRequests }, - { name: 'user-settings', path: '/user-settings', component: UserSettings } - ] - - const router = new VueRouter({ - mode: 'history', - routes, - scrollBehavior: (to, from, savedPosition) => { - if (to.matched.some(m => m.meta.dontScroll)) { - return false - } - return savedPosition || { x: 0, y: 0 } - } - }) - - /* eslint-disable no-new */ - new Vue({ - router, - store, - i18n, - el: '#app', - render: h => h(App) - }) - }) - }) - -window.fetch('/static/terms-of-service.html') - .then((res) => res.text()) - .then((html) => { - store.dispatch('setInstanceOption', { name: 'tos', value: html }) - }) - -window.fetch('/api/pleroma/emoji.json') - .then( - (res) => res.json() - .then( - (values) => { - const emoji = Object.keys(values).map((key) => { - return { shortcode: key, image_url: values[key] } - }) - store.dispatch('setInstanceOption', { name: 'customEmoji', value: emoji }) - store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: true }) - }, - (failure) => { - store.dispatch('setInstanceOption', { name: 'pleromaBackend', value: false }) - } - ), - (error) => console.log(error) - ) - -window.fetch('/static/emoji.json') - .then((res) => res.json()) - .then((values) => { - const emoji = Object.keys(values).map((key) => { - return { shortcode: key, image_url: false, 'utf': values[key] } - }) - store.dispatch('setInstanceOption', { name: 'emoji', value: emoji }) - }) - -window.fetch('/instance/panel.html') - .then((res) => res.text()) - .then((html) => { - store.dispatch('setInstanceOption', { name: 'instanceSpecificPanelContent', value: html }) +======= +const persistedStateOptions = { + paths: [ + 'config', + 'users.lastLoginName', + 'statuses.notifications.maxSavedId', + 'oauth' + ] +} +createPersistedState(persistedStateOptions).then((persistedState) => { + const store = new Vuex.Store({ + modules: { + interface: interfaceModule, + instance: instanceModule, + statuses: statusesModule, + users: usersModule, + api: apiModule, + config: configModule, + chat: chatModule, + oauth: oauthModule + }, + plugins: [persistedState], + strict: false // Socket modifies itself, let's ignore this for now. + // strict: process.env.NODE_ENV !== 'production' +>>>>>>> upstream/develop }) -window.fetch('/nodeinfo/2.0.json') - .then((res) => res.json()) - .then((data) => { - const metadata = data.metadata - - const features = metadata.features - store.dispatch('setInstanceOption', { name: 'mediaProxyAvailable', value: features.includes('media_proxy') }) - store.dispatch('setInstanceOption', { name: 'chatAvailable', value: features.includes('chat') }) - store.dispatch('setInstanceOption', { name: 'gopherAvailable', value: features.includes('gopher') }) - - const suggestions = metadata.suggestions - store.dispatch('setInstanceOption', { name: 'suggestionsEnabled', value: suggestions.enabled }) - store.dispatch('setInstanceOption', { name: 'suggestionsWeb', value: suggestions.web }) - }) + afterStoreSetup({store, i18n}) +}) diff --git a/src/modules/instance.js b/src/modules/instance.js index e5733831..9a39cccf 100644 --- a/src/modules/instance.js +++ b/src/modules/instance.js @@ -23,6 +23,7 @@ const defaultState = { disableChat: false, scopeCopy: true, subjectLineBehavior: 'email', + loginMethod: 'password', // Nasty stuff pleromaBackend: true, diff --git a/src/modules/oauth.js b/src/modules/oauth.js new file mode 100644 index 00000000..144ff830 --- /dev/null +++ b/src/modules/oauth.js @@ -0,0 +1,18 @@ +const oauth = { + state: { + client_id: false, + client_secret: false, + token: false + }, + mutations: { + setClientData (state, data) { + state.client_id = data.client_id + state.client_secret = data.client_secret + }, + setToken (state, token) { + state.token = token + } + } +} + +export default oauth diff --git a/src/modules/statuses.js b/src/modules/statuses.js index f980f53d..2c3d2550 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -41,7 +41,8 @@ export const defaultState = { own: emptyTl(), publicAndExternal: emptyTl(), friends: emptyTl(), - tag: emptyTl() + tag: emptyTl(), + dms: emptyTl() } } @@ -171,6 +172,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us sortTimeline(mentions) } } + if (status.visibility === 'direct') { + const dms = state.timelines.dms + + mergeOrAdd(dms.statuses, dms.statusesObject, status) + dms.newStatusCount += 1 + + sortTimeline(dms) + } } // Decide if we should treat the status as new for this timeline. diff --git a/src/modules/users.js b/src/modules/users.js index e90d6bb9..8630ee0d 100644 --- a/src/modules/users.js +++ b/src/modules/users.js @@ -82,24 +82,26 @@ const users = { }, logout (store) { store.commit('clearCurrentUser') + store.commit('setToken', false) store.dispatch('stopFetching', 'friends') store.commit('setBackendInteractor', backendInteractorService()) }, - loginUser (store, userCredentials) { + loginUser (store, accessToken) { return new Promise((resolve, reject) => { const commit = store.commit commit('beginLogin') - store.rootState.api.backendInteractor.verifyCredentials(userCredentials) + store.rootState.api.backendInteractor.verifyCredentials(accessToken) .then((response) => { if (response.ok) { response.json() .then((user) => { - user.credentials = userCredentials + // user.credentials = userCredentials + user.credentials = accessToken commit('setCurrentUser', user) commit('addNewUsers', [user]) // Set our new backend interactor - commit('setBackendInteractor', backendInteractorService(userCredentials)) + commit('setBackendInteractor', backendInteractorService(accessToken)) if (user.token) { store.dispatch('initializeSocket', user.token) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index ab746918..714c0fc7 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -15,6 +15,7 @@ const STATUS_URL = '/api/statuses/show' const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' const CONVERSATION_URL = '/api/statusnet/conversation' const MENTIONS_URL = '/api/statuses/mentions.json' +const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json' const FOLLOWERS_URL = '/api/statuses/followers.json' const FRIENDS_URL = '/api/statuses/friends.json' const FOLLOWING_URL = '/api/friendships/create.json' @@ -52,16 +53,6 @@ let fetch = (url, options) => { return oldfetch(fullUrl, options) } -// from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding -let utoa = (str) => { - // first we use encodeURIComponent to get percent-encoded UTF-8, - // then we convert the percent encodings into raw bytes which - // can be fed into btoa. - return btoa(encodeURIComponent(str) - .replace(/%([0-9A-F]{2})/g, - (match, p1) => { return String.fromCharCode('0x' + p1) })) -} - // Params // cropH // cropW @@ -175,9 +166,9 @@ const register = (params) => { }) } -const authHeaders = (user) => { - if (user && user.username && user.password) { - return { 'Authorization': `Basic ${utoa(`${user.username}:${user.password}`)}` } +const authHeaders = (accessToken) => { + if (accessToken) { + return { 'Authorization': `Bearer ${accessToken}` } } else { return { } } @@ -302,6 +293,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use public: PUBLIC_TIMELINE_URL, friends: FRIENDS_TIMELINE_URL, mentions: MENTIONS_URL, + dms: DM_TIMELINE_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL, 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, user: QVITTER_USER_TIMELINE_URL, diff --git a/src/services/new_api/oauth.js b/src/services/new_api/oauth.js new file mode 100644 index 00000000..9e656507 --- /dev/null +++ b/src/services/new_api/oauth.js @@ -0,0 +1,82 @@ +import {reduce} from 'lodash' + +const getOrCreateApp = ({oauth, instance}) => { + const url = `${instance}/api/v1/apps` + const form = new window.FormData() + + form.append('client_name', `PleromaFE_${Math.random()}`) + form.append('redirect_uris', `${window.location.origin}/oauth-callback`) + form.append('scopes', 'read write follow') + + return window.fetch(url, { + method: 'POST', + body: form + }).then((data) => data.json()) +} +const login = (args) => { + getOrCreateApp(args).then((app) => { + args.commit('setClientData', app) + + const data = { + response_type: 'code', + client_id: app.client_id, + redirect_uri: app.redirect_uri, + scope: 'read write follow' + } + + const dataString = reduce(data, (acc, v, k) => { + const encoded = `${k}=${encodeURIComponent(v)}` + if (!acc) { + return encoded + } else { + return `${acc}&${encoded}` + } + }, false) + + // Do the redirect... + const url = `${args.instance}/oauth/authorize?${dataString}` + + window.location.href = url + }) +} + +const getTokenWithCredentials = ({app, instance, username, password}) => { + const url = `${instance}/oauth/token` + const form = new window.FormData() + + form.append('client_id', app.client_id) + form.append('client_secret', app.client_secret) + form.append('grant_type', 'password') + form.append('username', username) + form.append('password', password) + + return window.fetch(url, { + method: 'POST', + body: form + }).then((data) => data.json()) +} + +const getToken = ({app, instance, code}) => { + const url = `${instance}/oauth/token` + const form = new window.FormData() + + form.append('client_id', app.client_id) + form.append('client_secret', app.client_secret) + form.append('grant_type', 'authorization_code') + form.append('code', code) + form.append('redirect_uri', `${window.location.origin}/oauth-callback`) + + return window.fetch(url, { + method: 'POST', + body: form + }).then((data) => data.json()) +} + +const oauth = { + login, + getToken, + getTokenWithCredentials, + getOrCreateApp +} + +export default oauth diff --git a/src/services/new_api/user_search.js b/src/services/new_api/user_search.js new file mode 100644 index 00000000..ce7da88e --- /dev/null +++ b/src/services/new_api/user_search.js @@ -0,0 +1,16 @@ +import utils from './utils.js' + +const search = ({query, store}) => { + return utils.request({ + store, + url: '/api/pleroma/search_user', + params: { + query + } + }).then((data) => data.json()) +} +const UserSearch = { + search +} + +export default UserSearch diff --git a/src/services/new_api/utils.js b/src/services/new_api/utils.js new file mode 100644 index 00000000..078f392f --- /dev/null +++ b/src/services/new_api/utils.js @@ -0,0 +1,36 @@ +const queryParams = (params) => { + return Object.keys(params) + .map(k => encodeURIComponent(k) + '=' + encodeURIComponent(params[k])) + .join('&') +} + +const headers = (store) => { + const accessToken = store.state.oauth.token + if (accessToken) { + return {'Authorization': `Bearer ${accessToken}`} + } else { + return {} + } +} + +const request = ({method = 'GET', url, params, store}) => { + const instance = store.state.instance.server + let fullUrl = `${instance}${url}` + + if (method === 'GET' && params) { + fullUrl = fullUrl + `?${queryParams(params)}` + } + + return window.fetch(fullUrl, { + method, + headers: headers(store), + credentials: 'same-origin' + }) +} + +const utils = { + queryParams, + request +} + +export default utils |
