diff --git a/package.json b/package.json index ee7a4af6c2b5ab368c1fbda397032aec648ffb5f..be176be5afbb7e6dd41099c69df0dbab6b7a97a9 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,9 @@ "core-js": "^2.6.5", "jquery": "^3.4.1", "lodash": "^4.17.11", + "material-icons": "^0.3.1", "materialize-css": "^1.0.0-rc.2", + "parse-link-header": "^1.0.1", "vue": "^2.6.10", "vue-router": "^3.0.3", "vuex": "^3.0.1", diff --git a/src/App.vue b/src/App.vue index 70e44fe2f583cf2f5b3aa4a21e5a302b864f9055..6c0bce141136d5e79a066f84480d4211aa81355f 100644 --- a/src/App.vue +++ b/src/App.vue @@ -27,4 +27,7 @@ </template> <style> +main { + padding: 1em 0; +} </style> diff --git a/src/components/Suggestions.vue b/src/components/Suggestions.vue new file mode 100644 index 0000000000000000000000000000000000000000..8744250b43126a3053c4e3a592caca4e8927aed8 --- /dev/null +++ b/src/components/Suggestions.vue @@ -0,0 +1,60 @@ +<template> + <div> + <ul class="collection with-header"> + <li class="collection-header"> + <h4>Connected accounts</h4> + </li> + <li class="collection-item avatar" v-for="account in accounts" :key="account.id"> + <img v-if="account._source.getAvatar(account)" :src="account._source.getAvatar(account)" alt="" class="circle"> + <span class="title">{{ account._source.getDisplayName(account) }}</span> + <p>{{ account._source.label }}</p> + <a href="#!" class="secondary-content"><i class="material-icons">grade</i></a> + </li> + </ul> + <h2>Suggestions</h2> + <button + @click="fetch" + :class="['waves-effect', 'waves-light', {disabled: isLoading}, 'btn-small']" :disabled="isLoading"> + <i class="material-icons left">refresh</i>Fetch data + </button> + <div v-if="isLoading" class="progress"> + <div class="indeterminate"></div> + </div> + </div> + +</template> + +<script> +import sources from '@/sources' +export default { + data () { + return { + isLoading: false, + results: {} + } + }, + computed: { + accounts () { + return this.$store.getters.sortedAccounts.map((a) => { + a._source = sources.sources[a.source] + return a + }) + } + }, + methods: { + async fetch () { + this.isLoading = true + this.accounts.forEach((a) => { + let r = {} + let promise = a._source.fetch({account: a, store: this.$store, results: r, vue: this}) + this.$set(this.results, a.id, {account: a, promise, results: r}) + }) + const keys = Object.keys(this.results) + for(let i = 0; i < keys.length; i++){ + await this.results[keys[i]].promise + } + this.isLoading = false + } + } +} +</script> diff --git a/src/main.js b/src/main.js index 707b3415b719452390ab0436978cc12ff5bab807..1aac278162e0bd0c9838ba8a4a9307bfab64419c 100644 --- a/src/main.js +++ b/src/main.js @@ -6,6 +6,7 @@ import store from './store' import "jquery" import "materialize-css/dist/css/materialize.min.css" import "materialize-css/dist/js/materialize.min.js" +import "material-icons/iconfont/material-icons.css" Vue.config.productionTip = false diff --git a/src/sources/mastodon.js b/src/sources/mastodon.js index 1125db73abe427e4131f177b61549167eb8da59a..040f3be10cee17b42d1ce15b6edeaa226ea3ff10 100644 --- a/src/sources/mastodon.js +++ b/src/sources/mastodon.js @@ -1,7 +1,8 @@ import axios from 'axios' import Form from '../components/MastodonForm.vue' +import parseLink from 'parse-link-header' -export const SCOPES = "read:accounts read:favorites read:follows read:statutes" +export const SCOPES = "read:accounts read:favourites read:follows read:statutes" export function favoritesToRetribute (favorites, cache) { if (!cache.profiles) { @@ -47,6 +48,15 @@ export default { authDataToKey ({username, domain}) { return `mastodon ${domain} ${username}` }, + getAvatar (data) { + if (!data.raw) { + return null + } + return data.raw.avatar + }, + getDisplayName ({username, domain}) { + return `${username}@${domain}` + }, async handleCallback({query, router, store, baseUrl}) { const domain = query.state console.log(`Received connect callback for mastodon domain ${domain}`) @@ -69,5 +79,40 @@ export default { const accountResponse = await client.get('/api/v1/accounts/verify_credentials') const username = `${accountResponse.data.acct}@${domain}` store.commit('setRecursiveState', {key: "sources.mastodon.appTokens", suffix: username, value: response.data}) + store.commit('addAccount', {source: 'mastodon', raw: accountResponse.data, username: accountResponse.data.acct, domain}) + router.push({path: '/'}) + }, + async fetch ({account, cache, store, results, vue}) { + console.log(`Fetching favorites for ${account.id}...`) + const token = store.state.sources.mastodon.appTokens[`${account.username}@${account.domain}`].access_token + const client = axios.create({ + baseURL: `https://${account.domain}`, + headers: {'Authorization': `Bearer ${token}`}, + }) + const maxFavorites = 100 + let url = '/api/v1/favourites' + let handledFavorites = 0 + vue.$set(results, 'accounts', {}) + while (handledFavorites < maxFavorites) { + let response = await client.get(url, {params: {limit: 40}}) + response.data.forEach((f) => { + handledFavorites += 1 + let account = f.account.acct + if (results.accounts[account]) { + results.accounts[account] += 1 + } else { + results.accounts[account] = 1 + } + }) + let link = response.headers.link || '' + let parsed = parseLink(link) + if (parsed && parsed.next) { + url = parsed.next.url + } else { + break + } + vue.$set(results, 'status', `Fetched favorites ${handledFavorites}/${maxFavorites}`) + } + return results } } diff --git a/src/store.js b/src/store.js index d9eb35b48c7678f4fac1c5f4b08ef58ba14081ed..40d00a7c5d7f8300ab5c3a31d651c618f0f095a1 100644 --- a/src/store.js +++ b/src/store.js @@ -3,6 +3,7 @@ import Vuex from 'vuex' import createPersistedState from 'vuex-persistedstate' import set from 'lodash/set' import get from 'lodash/get' +import sortBy from 'lodash/sortBy' import sources from '@/sources' Vue.use(Vuex) @@ -17,6 +18,7 @@ export const storeConfig = { mutations: { addAccount (state, payload) { const id = sources.sources[payload.source].authDataToKey(payload) + payload.id = id state.accounts[id] = payload }, setRecursiveState (state, {key, suffix, value}) { @@ -29,6 +31,12 @@ export const storeConfig = { } } }, + getters: { + sortedAccounts (state) { + let values = Object.values(state.accounts) + return sortBy(values, ["id"]) + }, + }, actions: { } diff --git a/src/views/Home.vue b/src/views/Home.vue index 640b0c550047bdb7972da3d7037312ed8f3b88f8..c2dd3f13672270a725a6f068a40008f1ecbfef78 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -12,14 +12,20 @@ </ol> <router-link to="/connect" class="waves-effect waves-light btn-large">Get started</router-link> </div> + <suggestions v-else></suggestions> </div> </template> <script> +import Suggestions from '@/components/Suggestions' + export default { + components: { + Suggestions + }, computed: { showIntroduction () { - return true + return this.$store.getters.sortedAccounts.length === 0 } } } diff --git a/yarn.lock b/yarn.lock index df76b1b04dda46fd75a0bddc019f45b791822119..c2858a28ef0c77a021d0ac847d4c242458bfa8db 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5146,6 +5146,11 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +material-icons@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/material-icons/-/material-icons-0.3.1.tgz#425e06e2448632d6249d6e1ffe2993682c5b171e" + integrity sha512-5Hbj76A6xDPcDZEbM4oxTknhWuMwGWnAHVLLPCEq9eVlcHb0fn4koU9ZeyMy1wjARtDEPAHfd5ZdL2Re5hf0zQ== + materialize-css@^1.0.0-rc.2: version "1.0.0-rc.2" resolved "https://registry.yarnpkg.com/materialize-css/-/materialize-css-1.0.0-rc.2.tgz#b9e6b18f698c6ef77bb6b628d59147faf131eff5" @@ -5997,6 +6002,13 @@ parse-json@^4.0.0: error-ex "^1.3.1" json-parse-better-errors "^1.0.1" +parse-link-header@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parse-link-header/-/parse-link-header-1.0.1.tgz#bedfe0d2118aeb84be75e7b025419ec8a61140a7" + integrity sha1-vt/g0hGK64S+deewJUGeyKYRQKc= + dependencies: + xtend "~4.0.1" + parse5@5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.0.tgz#c59341c9723f414c452975564c7c00a68d58acd2"