start on themes

This commit is contained in:
Nolan Lawson 2018-01-13 14:19:51 -08:00
parent fb9bc18edf
commit eaaacdeef5
15 changed files with 222 additions and 97 deletions

8
package-lock.json generated
View file

@ -2881,10 +2881,10 @@
} }
} }
}, },
"idb-keyval": { "idb": {
"version": "2.3.0", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-2.3.0.tgz", "resolved": "https://registry.npmjs.org/idb/-/idb-2.0.4.tgz",
"integrity": "sha1-TURLgMP4b8vNUTIbTcvJJHxZSMA=" "integrity": "sha512-Nw4ykKrrVje6YODRiRm/k2ucFEQeoY+zrkszfOuzVmxx8yyBMtZh2KLaRCKk9r5GzhuF0QlNCVjBewP2n5OZ7Q=="
}, },
"ieee754": { "ieee754": {
"version": "1.1.8", "version": "1.1.8",

View file

@ -21,7 +21,7 @@
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"font-awesome-svg-png": "^1.2.2", "font-awesome-svg-png": "^1.2.2",
"glob": "^7.1.2", "glob": "^7.1.2",
"idb-keyval": "^2.3.0", "idb": "^2.0.4",
"node-fetch": "^1.7.3", "node-fetch": "^1.7.3",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"npm-run-all": "^4.1.2", "npm-run-all": "^4.1.2",

View file

@ -5,11 +5,11 @@
</svg> </svg>
<h1>Pinafore</h1> <h1>Pinafore</h1>
</div> </div>
<p>Pinafore is a web client for <a href="https://joinmastodon.org">Mastodon</a>, optimized for speed and simplicity.</p> <p>Pinafore is a web client for <a rel="noopener" target="_blank" href="https://joinmastodon.org">Mastodon</a>, optimized for speed and simplicity.</p>
<p>To get started, <a href="/settings/instances/add">log in to an instance</a>.</p> <p>To get started, <a href="/settings/instances/add">log in to an instance</a>.</p>
<p>Don't have an instance? <a href="https://joinmastodon.org">Join Mastodon!</a></p> <p>Don't have an instance? <a rel="noopener" target="_blank" href="https://joinmastodon.org">Join Mastodon!</a></p>
</FreeTextLayout> </FreeTextLayout>
<style> <style>
.banner { .banner {

View file

@ -13,7 +13,7 @@
</style> </style>
<script> <script>
import { store } from '../_utils/store' import { store } from '../_utils/store'
import { getHomeTimeline } from '../_utils/mastodon' import { getHomeTimeline } from '../_utils/mastodon/oauth'
import fixture from '../_utils/fixture.json' import fixture from '../_utils/fixture.json'
import Status from './Status.html' import Status from './Status.html'

27
routes/_utils/ajax.js Normal file
View file

@ -0,0 +1,27 @@
export async function post(url, body) {
return await (await fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(body)
})).json()
}
export function paramsString(paramsObject) {
let params = new URLSearchParams()
Object.keys(paramsObject).forEach(key => {
params.set(key, paramsObject[value])
})
return params.toString()
}
export async function get(url, headers = {}) {
return await (await fetch(url, {
method: 'GET',
headers: Object.assign(headers, {
'Accept': 'application/json',
})
})).json()
}

View file

@ -1,21 +0,0 @@
import idbKeyVal from 'idb-keyval'
import { blobToBase64 } from '../_utils/binary'
let databasePromise
if (process.browser) {
databasePromise = Promise.resolve().then(async () => {
let token = await idbKeyVal.get('secure_token')
if (!token) {
let array = new Uint32Array(1028)
crypto.getRandomValues(array);
let token = await blobToBase64(new Blob([array]))
await idbKeyVal.set('secure_token', token)
}
return idbKeyVal
})
} else {
databasePromise = Promise.resolve()
}
export { databasePromise }

View file

@ -0,0 +1,2 @@
import keyval from './idb-keyval'

View file

@ -0,0 +1,57 @@
import idb from 'idb'
// copypasta'd from https://github.com/jakearchibald/idb#keyval-store
const dbPromise = idb.open('keyval-store', 1, upgradeDB => {
upgradeDB.createObjectStore('keyval')
})
const idbKeyval = {
get(key) {
return dbPromise.then(db => {
return db.transaction('keyval').objectStore('keyval').get(key)
})
},
set(key, val) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite')
tx.objectStore('keyval').put(val, key)
return tx.complete
})
},
delete(key) {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite')
tx.objectStore('keyval').delete(key)
return tx.complete
})
},
clear() {
return dbPromise.then(db => {
const tx = db.transaction('keyval', 'readwrite')
tx.objectStore('keyval').clear()
return tx.complete
})
},
keys() {
return dbPromise.then(db => {
const tx = db.transaction('keyval')
const keys = []
const store = tx.objectStore('keyval')
// This would be store.getAllKeys(), but it isn't supported by Edge or Safari.
// openKeyCursor isn't supported by Safari, so we fall back
const iterate = store.iterateKeyCursor || store.iterateCursor
iterate.call(store, cursor => {
if (!cursor) {
return
}
keys.push(cursor.key)
cursor.continue()
})
return tx.complete.then(() => keys)
})
}
}
export default idbKeyval

View file

@ -1,62 +0,0 @@
const WEBSITE = 'https://pinafore.social'
const REDIRECT_URI = (typeof location !== 'undefined' ? location.origin : 'https://pinafore.social') + '/settings/instances/add'
const SCOPES = 'read write follow'
const CLIENT_NAME = 'Pinafore'
export function registerApplication(instanceName) {
const url = `https://${instanceName}/api/v1/apps`
return fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_name: CLIENT_NAME,
redirect_uris: REDIRECT_URI,
scopes: SCOPES,
website: WEBSITE
})
})
}
export function generateAuthLink(instanceName, clientId) {
let url = `https://${instanceName}/oauth/authorize`
let params = new URLSearchParams()
params.set('client_id', clientId)
params.set('redirect_uri', REDIRECT_URI)
params.set('response_type', 'code')
params.set('scope', SCOPES)
url += '?' + params.toString()
return url
}
export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret, code) {
let url = `https://${instanceName}/oauth/token`
return fetch(url, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
client_id: clientId,
client_secret: clientSecret,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code: code
})
})
}
export function getHomeTimeline(instanceName, accessToken) {
let url = `https://${instanceName}/api/v1/timelines/home`
return fetch(url, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Authorization': `Bearer ${accessToken}`
}
})
}

View file

@ -0,0 +1,43 @@
const WEBSITE = 'https://pinafore.social'
const REDIRECT_URI = (typeof location !== 'undefined' ? location.origin : 'https://pinafore.social') + '/settings/instances'
const SCOPES = 'read write follow'
const CLIENT_NAME = 'Pinafore'
import { post, get, paramsString } from '../ajax'
export function registerApplication(instanceName) {
const url = `https://${instanceName}/api/v1/apps`
return post(url, {
client_name: CLIENT_NAME,
redirect_uris: REDIRECT_URI,
scopes: SCOPES,
website: WEBSITE
})
}
export function generateAuthLink(instanceName, clientId) {
let params = paramsString({
'client_id': clientId,
'redirect_uri': REDIRECT_URI,
'response_type': 'code',
'scope': SCOPES
})
return `https://${instanceName}/oauth/authorize?${params}`
}
export function getAccessTokenFromAuthCode(instanceName, clientId, clientSecret, code) {
let url = `https://${instanceName}/oauth/token`
return post(url, {
client_id: clientId,
client_secret: clientSecret,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code: code
})
}
export function getHomeTimeline(instanceName, accessToken) {
let url = `https://${instanceName}/api/v1/timelines/home`
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}

View file

@ -0,0 +1,8 @@
import { get } from '../ajax'
export function getCurrentUser(instanceName, accessToken) {
let url = `https://${instanceName}/api/v1/accounts/verify_credentials`
return get(url, {
'Authorization': `Bearer ${accessToken}`
})
}

View file

@ -65,6 +65,15 @@ store.compute(
} }
) )
store.compute(
'currentInstanceData',
['currentInstance', 'loggedInInstances'],
(currentInstance, loggedInInstances) => {
return Object.assign({
name: currentInstance
}, loggedInInstances[currentInstance])
})
if (process.browser && process.env.NODE_ENV !== 'production') { if (process.browser && process.env.NODE_ENV !== 'production') {
window.store = store // for debugging window.store = store // for debugging
} }

View file

@ -12,6 +12,9 @@
:global(.settings .free-text h1) { :global(.settings .free-text h1) {
margin-bottom: 30px; margin-bottom: 30px;
} }
:global(.settings .free-text h2) {
margin: 20px 0 10px;
}
</style> </style>
<script> <script>
import SettingsNav from './SettingsNav.html'; import SettingsNav from './SettingsNav.html';

View file

@ -5,18 +5,78 @@
<Layout page='settings'> <Layout page='settings'>
<SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}"> <SettingsLayout page='settings/instances/{{params.instanceName}}' label="{{params.instanceName}}">
<h1>{{params.instanceName}}</h1> <h1>{{params.instanceName}}</h1>
{{#if currentUser}}
<h2>Logged in as:</h2>
<div class="current-user">
<img src="{{currentUser.avatar}}" />
<a rel="noopener" target="_blank" href="{{currentUser.url}}">@{{currentUser.acct}}</a>
<span class="acct-name">{{currentUser.display_name}}</span>
</div>
<h2>Theme:</h2>
<form class="theme-chooser">
<div class="theme-group">
<input type="radio" name="current-theme" id="theme-default" value="default">
<label for="theme-default">Royal (default)</label>
</div>
<div class="theme-group">
<input type="radio" name="current-theme" id="theme-crimson"
value="crimson">
<label for="theme-crimson">Crimson and Clover</label>
</div>
<div class="theme-group">
<input type="radio" name="current-theme" id="theme-hotpants"
value="hotpants">
<label for="theme-hotpants">Hot Pants</label>
</div>
</form>
{{/if}}
</SettingsLayout> </SettingsLayout>
</Layout> </Layout>
<style>
.current-user {
padding: 20px;
display: flex;
align-items: center;
font-size: 1.3em;
}
.current-user img {
width: 64px;
height: 64px;
border-radius: 4px;
}
.current-user img, .current-user a, .current-user span {
margin-right: 20px;
}
.theme-chooser {
display: block;
padding: 20px;
line-height: 2em;
}
.theme-group {
display: flex;
align-items: center;
}
.theme-chooser label {
margin: 2px 10px 0;
}
</style>
<script> <script>
import { store } from '../../_utils/store' import { store } from '../../_utils/store'
import Layout from '../../_components/Layout.html' import Layout from '../../_components/Layout.html'
import SettingsLayout from '../_components/SettingsLayout.html' import SettingsLayout from '../_components/SettingsLayout.html'
import { getCurrentUser } from '../../_utils/mastodon/user'
export default { export default {
components: { components: {
Layout, Layout,
SettingsLayout SettingsLayout
}, },
store: () => store store: () => store,
oncreate: async function () {
let currentInstanceData = this.store.get('currentInstanceData')
let currentUser = await getCurrentUser(currentInstanceData.name, currentInstanceData.access_token)
this.set({currentUser: currentUser})
}
} }
</script> </script>

View file

@ -19,7 +19,7 @@
</form> </form>
{{#if !$isUserLoggedIn}} {{#if !$isUserLoggedIn}}
<p>Don't have an instance? <a href="https://joinmastodon.org">Join Mastodon!</a></p> <p>Don't have an instance? <a rel="noopener" target="_blank" href="https://joinmastodon.org">Join Mastodon!</a></p>
{{/if}} {{/if}}
</SettingsLayout> </SettingsLayout>
</Layout> </Layout>
@ -49,8 +49,7 @@
<script> <script>
import Layout from '../../_components/Layout.html'; import Layout from '../../_components/Layout.html';
import SettingsLayout from '../_components/SettingsLayout.html' import SettingsLayout from '../_components/SettingsLayout.html'
import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon' import { registerApplication, generateAuthLink, getAccessTokenFromAuthCode } from '../../_utils/mastodon/oauth'
import { databasePromise } from '../../_utils/database'
import { store } from '../../_utils/store' import { store } from '../../_utils/store'
import { goto } from 'sapper/runtime.js' import { goto } from 'sapper/runtime.js'
@ -74,12 +73,12 @@
onReceivedOauthCode: async function(code) { onReceivedOauthCode: async function(code) {
let currentRegisteredInstanceName = this.store.get('currentRegisteredInstanceName') let currentRegisteredInstanceName = this.store.get('currentRegisteredInstanceName')
let currentRegisteredInstance = this.store.get('currentRegisteredInstance') let currentRegisteredInstance = this.store.get('currentRegisteredInstance')
let instanceData = await (await getAccessTokenFromAuthCode( let instanceData = await getAccessTokenFromAuthCode(
currentRegisteredInstanceName, currentRegisteredInstanceName,
currentRegisteredInstance.client_id, currentRegisteredInstance.client_id,
currentRegisteredInstance.client_secret, currentRegisteredInstance.client_secret,
code code
)).json() )
// TODO: handle error // TODO: handle error
let loggedInInstances = this.store.get('loggedInInstances') let loggedInInstances = this.store.get('loggedInInstances')
let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder') let loggedInInstancesInOrder = this.store.get('loggedInInstancesInOrder')