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.
This commit is contained in:
Henry Jameson 2018-11-26 04:38:44 +03:00
commit e06717fd0d
32 changed files with 1576 additions and 279 deletions

View file

@ -32,3 +32,9 @@ npm run unit
# Configuration # Configuration
Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings. Edit config.json for configuration. scopeOptionsEnabled gives you input fields for CWs and the scope settings.
## Options
### Login methods
```loginMethod``` can be set to either ```password``` (the default) or ```token```, which will use the full oauth redirection flow, which is useful for SSO situations.

View file

@ -23,17 +23,17 @@ module.exports = {
assetsPublicPath: '/', assetsPublicPath: '/',
proxyTable: { proxyTable: {
'/api': { '/api': {
target: 'http://localhost:4000/', target: 'https://shigusegubu.club/',
changeOrigin: true, changeOrigin: true,
cookieDomainRewrite: 'localhost' cookieDomainRewrite: 'localhost'
}, },
'/nodeinfo': { '/nodeinfo': {
target: 'http://localhost:4000/', target: 'https://shigusegubu.club/',
changeOrigin: true, changeOrigin: true,
cookieDomainRewrite: 'localhost' cookieDomainRewrite: 'localhost'
}, },
'/socket': { '/socket': {
target: 'http://localhost:4000/', target: 'https://shigusegubu.club/',
changeOrigin: true, changeOrigin: true,
cookieDomainRewrite: 'localhost', cookieDomainRewrite: 'localhost',
ws: true ws: true

View file

@ -73,6 +73,7 @@ export default {
window.scrollTo(0, 0) window.scrollTo(0, 0)
}, },
logout () { logout () {
this.$router.replace('/main/public')
this.$store.dispatch('logout') this.$store.dispatch('logout')
} }
} }

184
src/boot/after_store.js Normal file
View file

@ -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

View file

@ -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

View file

@ -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>

View file

@ -1,22 +1,40 @@
import oauthApi from '../../services/new_api/oauth.js'
const LoginForm = { const LoginForm = {
data: () => ({ data: () => ({
user: {}, user: {},
authError: false authError: false
}), }),
computed: { computed: {
loginMethod () { return this.$store.state.instance.loginMethod },
loggingIn () { return this.$store.state.users.loggingIn }, loggingIn () { return this.$store.state.users.loggingIn },
registrationOpen () { return this.$store.state.instance.registrationOpen } registrationOpen () { return this.$store.state.instance.registrationOpen }
}, },
methods: { methods: {
oAuthLogin () {
oauthApi.login({
oauth: this.$store.state.oauth,
instance: this.$store.state.instance.server,
commit: this.$store.commit
})
},
submit () { submit () {
this.$store.dispatch('loginUser', this.user).then( const data = {
() => {}, oauth: this.$store.state.oauth,
(error) => { instance: this.$store.state.instance.server
this.authError = error }
this.user.username = '' oauthApi.getOrCreateApp(data).then((app) => {
this.user.password = '' 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')
})
})
} }
} }
} }

View file

@ -5,7 +5,7 @@
{{$t('login.login')}} {{$t('login.login')}}
</div> </div>
<div class="panel-body"> <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'> <div class='form-group'>
<label for='username'>{{$t('login.username')}}</label> <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')"> <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> <button :disabled="loggingIn" type='submit' class='btn btn-default'>{{$t('login.login')}}</button>
</div> </div>
</div> </div>
<div v-if="authError" class='form-group'> </form>
<div class='alert error'>{{authError}}</div>
<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> </div>
</form> </form>
</div> </div>

View file

@ -12,6 +12,11 @@
{{ $t("nav.mentions") }} {{ $t("nav.mentions") }}
</router-link> </router-link>
</li> </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'> <li v-if='currentUser && currentUser.locked'>
<router-link to='/friend-requests'> <router-link to='/friend-requests'>
{{ $t("nav.friend_requests") }} {{ $t("nav.friend_requests") }}

View file

@ -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

View file

@ -0,0 +1,5 @@
<template>
<h1>...</h1>
</template>
<script src="./oauth_callback.js"></script>

View file

@ -1,3 +1,5 @@
import oauthApi from '../../services/new_api/oauth.js'
const registration = { const registration = {
data: () => ({ data: () => ({
user: {}, user: {},
@ -25,9 +27,23 @@ const registration = {
this.$store.state.api.backendInteractor.register(this.user).then( this.$store.state.api.backendInteractor.register(this.user).then(
(response) => { (response) => {
if (response.ok) { if (response.ok) {
this.$store.dispatch('loginUser', this.user) const data = {
this.$router.push('/main/all') oauth: this.$store.state.oauth,
this.registering = false 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 { } else {
this.registering = false this.registering = false
response.json().then((data) => { response.json().then((data) => {

View file

@ -7,25 +7,10 @@ const UserFinder = {
}), }),
methods: { methods: {
findUser (username) { findUser (username) {
username = username[0] === '@' ? username.slice(1) : username this.$router.push({ name: 'user-search', query: { query: 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
}
})
}, },
toggleHidden () { toggleHidden () {
this.hidden = !this.hidden this.hidden = !this.hidden
},
dismissError () {
this.error = false
} }
} }
} }

View file

@ -1,9 +1,5 @@
<template> <template>
<span class="user-finder-container"> <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" /> <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> <a href="#" v-if="hidden"><i class="icon-user-plus user-finder-icon" @click.prevent.stop="toggleHidden"/></a>
<span v-else> <span v-else>

View file

@ -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

View file

@ -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>

View file

@ -21,6 +21,7 @@
}, },
"login": { "login": {
"login": "Anmelden", "login": "Anmelden",
"description": "Mit OAuth anmelden",
"logout": "Abmelden", "logout": "Abmelden",
"password": "Passwort", "password": "Passwort",
"placeholder": "z.B. lain", "placeholder": "z.B. lain",

View file

@ -21,6 +21,7 @@
}, },
"login": { "login": {
"login": "Log in", "login": "Log in",
"description": "Log in with OAuth",
"logout": "Log out", "logout": "Log out",
"password": "Password", "password": "Password",
"placeholder": "e.g. lain", "placeholder": "e.g. lain",
@ -31,9 +32,11 @@
"chat": "Local Chat", "chat": "Local Chat",
"friend_requests": "Follow Requests", "friend_requests": "Follow Requests",
"mentions": "Mentions", "mentions": "Mentions",
"dms": "Direct Messages",
"public_tl": "Public Timeline", "public_tl": "Public Timeline",
"timeline": "Timeline", "timeline": "Timeline",
"twkn": "The Whole Known Network" "twkn": "The Whole Known Network",
"user_search": "User Search"
}, },
"notifications": { "notifications": {
"broken_favorite": "Unknown status, searching for it...", "broken_favorite": "Unknown status, searching for it...",

View file

@ -21,6 +21,7 @@
}, },
"login": { "login": {
"login": "Connexion", "login": "Connexion",
"description": "Connexion avec OAuth",
"logout": "Déconnexion", "logout": "Déconnexion",
"password": "Mot de passe", "password": "Mot de passe",
"placeholder": "p.e. lain", "placeholder": "p.e. lain",
@ -30,6 +31,7 @@
"nav": { "nav": {
"chat": "Chat local", "chat": "Chat local",
"friend_requests": "Demandes d'ami", "friend_requests": "Demandes d'ami",
"dms": "Messages adressés",
"mentions": "Notifications", "mentions": "Notifications",
"public_tl": "Statuts locaux", "public_tl": "Statuts locaux",
"timeline": "Journal", "timeline": "Journal",
@ -86,7 +88,7 @@
"cRed": "Rouge (Annuler)", "cRed": "Rouge (Annuler)",
"change_password": "Changez votre mot de passe", "change_password": "Changez votre mot de passe",
"change_password_error": "Il y a eu un problème pour changer 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", "collapse_subject": "Réduire les messages avec des sujets",
"confirm_new_password": "Confirmation du nouveau mot de passe", "confirm_new_password": "Confirmation du nouveau mot de passe",
"current_avatar": "Avatar actuel", "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.", "delete_account_instructions": "Indiquez votre mot de passe ci-dessous pour confirmer la suppression de votre compte.",
"export_theme": "Enregistrer le thème", "export_theme": "Enregistrer le thème",
"filtering": "Filtre", "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": "Exporter les abonnements",
"follow_export_button": "Exporter les abonnements en csv", "follow_export_button": "Exporter les abonnements en csv",
"follow_export_processing": "Exportation en cours…", "follow_export_processing": "Exportation en cours…",
"follow_import": "Importer des abonnements", "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.", "follows_imported": "Abonnements importés ! Le traitement peut prendre un moment.",
"foreground": "Premier plan", "foreground": "Premier plan",
"general": "Général", "general": "Général",
"hide_attachments_in_convo": "Masquer les pièces jointes dans les conversations", "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_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_followers_from_a_csv_file": "Importer des abonnements depuis un fichier csv",
"import_theme": "Charger le thème", "import_theme": "Charger le thème",
"inputRadius": "Champs de texte", "inputRadius": "Champs de texte",
@ -156,8 +160,8 @@
"streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page", "streaming": "Charger automatiquement les nouveaux statuts lorsque vous êtes au haut de la page",
"text": "Texte", "text": "Texte",
"theme": "Thème", "theme": "Thème",
"theme_help": "Spécifiez des codes couleur hexadécimaux (#aabbcc) pour personnaliser les couleurs du thème", "theme_help": "Spécifiez des codes couleur hexadécimaux (#rrvvbb) pour personnaliser les couleurs du thème.",
"tooltipRadius": "Info-bulles/alertes ", "tooltipRadius": "Info-bulles/alertes",
"user_settings": "Paramètres utilisateur", "user_settings": "Paramètres utilisateur",
"values": { "values": {
"false": "non", "false": "non",
@ -177,7 +181,7 @@
"user_card": { "user_card": {
"approve": "Accepter", "approve": "Accepter",
"block": "Bloquer", "block": "Bloquer",
"blocked": "Bloqué", "blocked": "Bloqué !",
"deny": "Rejeter", "deny": "Rejeter",
"follow": "Suivre", "follow": "Suivre",
"followees": "Suivis", "followees": "Suivis",

View file

@ -21,6 +21,7 @@
}, },
"login": { "login": {
"login": "ログイン", "login": "ログイン",
"description": "OAuthでログイン",
"logout": "ログアウト", "logout": "ログアウト",
"password": "パスワード", "password": "パスワード",
"placeholder": "れい: lain", "placeholder": "れい: lain",
@ -31,6 +32,7 @@
"chat": "ローカルチャット", "chat": "ローカルチャット",
"friend_requests": "フォローリクエスト", "friend_requests": "フォローリクエスト",
"mentions": "メンション", "mentions": "メンション",
"dms": "ダイレクトメッセージ",
"public_tl": "パブリックタイムライン", "public_tl": "パブリックタイムライン",
"timeline": "タイムライン", "timeline": "タイムライン",
"twkn": "つながっているすべてのネットワーク" "twkn": "つながっているすべてのネットワーク"
@ -111,6 +113,8 @@
"general": "ぜんぱん", "general": "ぜんぱん",
"hide_attachments_in_convo": "スレッドのファイルをかくす", "hide_attachments_in_convo": "スレッドのファイルをかくす",
"hide_attachments_in_tl": "タイムラインのファイルをかくす", "hide_attachments_in_tl": "タイムラインのファイルをかくす",
"hide_post_stats": "とうこうのとうけいをかくす (れい: おきにいりのかず)",
"hide_user_stats": "ユーザーのとうけいをかくす (れい: フォロワーのかず)",
"import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする", "import_followers_from_a_csv_file": "CSVファイルからフォローをインポートする",
"import_theme": "ロード", "import_theme": "ロード",
"inputRadius": "インプットフィールド", "inputRadius": "インプットフィールド",

View file

@ -17,7 +17,9 @@ const saveImmedeatelyActions = [
'clearCurrentUser', 'clearCurrentUser',
'setCurrentUser', 'setCurrentUser',
'setHighlight', 'setHighlight',
'setOption' 'setOption',
'setClientData',
'setToken'
] ]
const defaultStorage = (() => { const defaultStorage = (() => {
@ -43,8 +45,8 @@ export default function createPersistedState ({
storage = defaultStorage, storage = defaultStorage,
subscriber = store => handler => store.subscribe(handler) subscriber = store => handler => store.subscribe(handler)
} = {}) { } = {}) {
return store => { return getState(key, storage).then((savedState) => {
getState(key, storage).then((savedState) => { return store => {
try { try {
if (typeof savedState === 'object') { if (typeof savedState === 'object') {
// build user cache // build user cache
@ -67,36 +69,35 @@ export default function createPersistedState ({
value: store.state.config.customTheme value: store.state.config.customTheme
}) })
} }
if (store.state.users.lastLoginName) { if (store.state.oauth.token) {
store.dispatch('loginUser', {username: store.state.users.lastLoginName, password: 'xxx'}) store.dispatch('loginUser', store.state.oauth.token)
} }
loaded = true loaded = true
} catch (e) { } catch (e) {
console.log("Couldn't load state") console.log("Couldn't load state")
loaded = true loaded = true
} }
}) subscriber(store)((mutation, state) => {
try {
subscriber(store)((mutation, state) => { if (saveImmedeatelyActions.includes(mutation.type)) {
try { setState(key, reducer(state, paths), storage)
if (saveImmedeatelyActions.includes(mutation.type)) { .then(success => {
setState(key, reducer(state, paths), storage) if (typeof success !== 'undefined') {
.then(success => { if (mutation.type === 'setOption') {
if (typeof success !== 'undefined') { store.dispatch('settingsSaved', { success })
if (mutation.type === 'setOption') { }
store.dispatch('settingsSaved', { success })
} }
} }, error => {
}, error => { if (mutation.type === 'setOption') {
if (mutation.type === 'setOption') { store.dispatch('settingsSaved', { error })
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) })
}
})
}
} }

View file

@ -1,18 +1,6 @@
import Vue from 'vue' import Vue from 'vue'
import VueRouter from 'vue-router' import VueRouter from 'vue-router'
import Vuex from 'vuex' 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 interfaceModule from './modules/interface.js'
import instanceModule from './modules/instance.js' import instanceModule from './modules/instance.js'
@ -21,6 +9,7 @@ import usersModule from './modules/users.js'
import apiModule from './modules/api.js' import apiModule from './modules/api.js'
import configModule from './modules/config.js' import configModule from './modules/config.js'
import chatModule from './modules/chat.js' import chatModule from './modules/chat.js'
import oauthModule from './modules/oauth.js'
import VueTimeago from 'vue-timeago' import VueTimeago from 'vue-timeago'
import VueI18n from 'vue-i18n' import VueI18n from 'vue-i18n'
@ -31,6 +20,8 @@ import messages from './i18n/messages.js'
import VueChatScroll from 'vue-chat-scroll' import VueChatScroll from 'vue-chat-scroll'
import afterStoreSetup from './boot/after_store.js'
const currentLocale = (window.navigator.language || 'en').split('-')[0] const currentLocale = (window.navigator.language || 'en').split('-')[0]
Vue.use(Vuex) Vue.use(Vuex)
@ -45,29 +36,6 @@ Vue.use(VueTimeago, {
Vue.use(VueI18n) Vue.use(VueI18n)
Vue.use(VueChatScroll) 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({ const i18n = new VueI18n({
// By default, use the browser locale, we will update it if neccessary // By default, use the browser locale, we will update it if neccessary
locale: currentLocale, locale: currentLocale,
@ -75,159 +43,32 @@ const i18n = new VueI18n({
messages messages
}) })
window.fetch('/api/statusnet/config.json') =======
.then((res) => res.json()) const persistedStateOptions = {
.then((data) => { paths: [
const {name, closed: registrationClosed, textlimit, server} = data.site 'config',
'users.lastLoginName',
store.dispatch('setInstanceOption', { name: 'name', value: name }) 'statuses.notifications.maxSavedId',
store.dispatch('setInstanceOption', { name: 'registrationOpen', value: (registrationClosed === '0') }) 'oauth'
store.dispatch('setInstanceOption', { name: 'textlimit', value: parseInt(textlimit) }) ]
store.dispatch('setInstanceOption', { name: 'server', value: server }) }
createPersistedState(persistedStateOptions).then((persistedState) => {
var apiConfig = data.site.pleromafe const store = new Vuex.Store({
modules: {
window.fetch('/static/config.json') interface: interfaceModule,
.then((res) => res.json()) instance: instanceModule,
.catch((err) => { statuses: statusesModule,
console.warn('Failed to load static/config.json, continuing without it.') users: usersModule,
console.warn(err) api: apiModule,
return {} config: configModule,
}) chat: chatModule,
.then((staticConfig) => { oauth: oauthModule
// This takes static config and overrides properties that are present in apiConfig },
var config = Object.assign({}, staticConfig, apiConfig) plugins: [persistedState],
strict: false // Socket modifies itself, let's ignore this for now.
var theme = (config.theme) // strict: process.env.NODE_ENV !== 'production'
var background = (config.background) >>>>>>> upstream/develop
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') afterStoreSetup({store, i18n})
.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 })
})

View file

@ -23,6 +23,7 @@ const defaultState = {
disableChat: false, disableChat: false,
scopeCopy: true, scopeCopy: true,
subjectLineBehavior: 'email', subjectLineBehavior: 'email',
loginMethod: 'password',
// Nasty stuff // Nasty stuff
pleromaBackend: true, pleromaBackend: true,

18
src/modules/oauth.js Normal file
View file

@ -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

View file

@ -41,7 +41,8 @@ export const defaultState = {
own: emptyTl(), own: emptyTl(),
publicAndExternal: emptyTl(), publicAndExternal: emptyTl(),
friends: emptyTl(), friends: emptyTl(),
tag: emptyTl() tag: emptyTl(),
dms: emptyTl()
} }
} }
@ -171,6 +172,14 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
sortTimeline(mentions) 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. // Decide if we should treat the status as new for this timeline.

View file

@ -82,24 +82,26 @@ const users = {
}, },
logout (store) { logout (store) {
store.commit('clearCurrentUser') store.commit('clearCurrentUser')
store.commit('setToken', false)
store.dispatch('stopFetching', 'friends') store.dispatch('stopFetching', 'friends')
store.commit('setBackendInteractor', backendInteractorService()) store.commit('setBackendInteractor', backendInteractorService())
}, },
loginUser (store, userCredentials) { loginUser (store, accessToken) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const commit = store.commit const commit = store.commit
commit('beginLogin') commit('beginLogin')
store.rootState.api.backendInteractor.verifyCredentials(userCredentials) store.rootState.api.backendInteractor.verifyCredentials(accessToken)
.then((response) => { .then((response) => {
if (response.ok) { if (response.ok) {
response.json() response.json()
.then((user) => { .then((user) => {
user.credentials = userCredentials // user.credentials = userCredentials
user.credentials = accessToken
commit('setCurrentUser', user) commit('setCurrentUser', user)
commit('addNewUsers', [user]) commit('addNewUsers', [user])
// Set our new backend interactor // Set our new backend interactor
commit('setBackendInteractor', backendInteractorService(userCredentials)) commit('setBackendInteractor', backendInteractorService(accessToken))
if (user.token) { if (user.token) {
store.dispatch('initializeSocket', user.token) store.dispatch('initializeSocket', user.token)

View file

@ -15,6 +15,7 @@ const STATUS_URL = '/api/statuses/show'
const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload'
const CONVERSATION_URL = '/api/statusnet/conversation' const CONVERSATION_URL = '/api/statusnet/conversation'
const MENTIONS_URL = '/api/statuses/mentions.json' const MENTIONS_URL = '/api/statuses/mentions.json'
const DM_TIMELINE_URL = '/api/statuses/dm_timeline.json'
const FOLLOWERS_URL = '/api/statuses/followers.json' const FOLLOWERS_URL = '/api/statuses/followers.json'
const FRIENDS_URL = '/api/statuses/friends.json' const FRIENDS_URL = '/api/statuses/friends.json'
const FOLLOWING_URL = '/api/friendships/create.json' const FOLLOWING_URL = '/api/friendships/create.json'
@ -52,16 +53,6 @@ let fetch = (url, options) => {
return oldfetch(fullUrl, 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 // Params
// cropH // cropH
// cropW // cropW
@ -175,9 +166,9 @@ const register = (params) => {
}) })
} }
const authHeaders = (user) => { const authHeaders = (accessToken) => {
if (user && user.username && user.password) { if (accessToken) {
return { 'Authorization': `Basic ${utoa(`${user.username}:${user.password}`)}` } return { 'Authorization': `Bearer ${accessToken}` }
} else { } else {
return { } return { }
} }
@ -302,6 +293,7 @@ const fetchTimeline = ({timeline, credentials, since = false, until = false, use
public: PUBLIC_TIMELINE_URL, public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL, friends: FRIENDS_TIMELINE_URL,
mentions: MENTIONS_URL, mentions: MENTIONS_URL,
dms: DM_TIMELINE_URL,
notifications: QVITTER_USER_NOTIFICATIONS_URL, notifications: QVITTER_USER_NOTIFICATIONS_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL, 'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL,
user: QVITTER_USER_TIMELINE_URL, user: QVITTER_USER_TIMELINE_URL,

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -14,5 +14,6 @@
"scopeCopy": false, "scopeCopy": false,
"subjectLineBehavior": "email", "subjectLineBehavior": "email",
"hidePostStats": false, "hidePostStats": false,
"hideUserStats": false "hideUserStats": false,
"loginMethod": "password"
} }

977
yarn.lock

File diff suppressed because it is too large Load diff