Page MenuHomePhorge

No OneTemporary

Size
21 KB
Referenced Files
None
Subscribers
None
diff --git a/src/App.scss b/src/App.scss
index c155de730b..a97ad56de3 100644
--- a/src/App.scss
+++ b/src/App.scss
@@ -1,291 +1,289 @@
-$main-color: #f58d2c;
-$main-background: white;
-$darkened-background: whitesmoke;
-
+@import './_variables.scss';
#app {
background-color: $main-color;
background-size: cover;
background-attachment: fixed;
background-repeat: no-repeat;
background-position: 0 50px;
min-height: 100vh;
}
h4 {
margin: 0;
}
#content {
padding-top: 60px;
}
.text-center {
text-align: center;
}
body {
font-family: sans-serif;
font-size: 14px;
margin: 0;
}
a {
text-decoration: none;
color: $main-color;
}
.container {
display: flex;
flex-wrap: wrap;
margin: 0;
padding: 0 10px 0 10px;
}
.gaps {
margin: -1em 0 0 -1em;
}
.item {
flex: 1;
}
.gaps > .item {
padding: 1em 0 0 1em;
}
.auto-size {
flex: 1
}
nav {
background: black;
width: 100%;
align-items: center;
position: fixed;
height: 50px;
}
sidebar {
flex: 1;
flex-basis: 300px;
}
main-router {
flex: 1;
}
.status:hover {
background-color: $darkened-background;
}
/* The starting CSS styles for the enter animation */
status.ng-enter {
transition:0.5s linear all;
opacity:0;
}
/* The finishing CSS styles for the enter animation */
status.ng-enter.ng-enter-active {
opacity:1;
}
.new-status-notification {
font-size: 1.1em;
background-color: $darkened-background;
border-bottom-color: darken($darkened-background, 5%);
border-bottom-style: solid;
border-bottom-width: 1px;
&:hover {
background-color: darken($darkened-background, 5%);
}
p {
margin: 0px;
padding: 10px;
}
}
.status.compact {
color: rgba(0, 0, 0, 0.42);
font-weight: 300;
p {
margin: 0;
font-size: 0.8em
}
}
/* Panel */
.panel {
display: flex;
flex-direction: column;
background-color: $main-background;
margin: 0.5em;
border-radius: 0.5em;
}
.panel-heading {
border-radius: 0.5em 0.5em 0 0;
background-size: cover;
background-color: bisque;
padding-top: 0.3em;
padding-bottom: 0.3em;
text-align: center;
font-size: 1.3em;
}
.panel-footer {
background-color: bisque;
border-radius: 0 0 0.5em 0.5em;
}
.panel-body > p {
margin: 1em;
}
.media-body {
flex: 1
}
#content {
margin: auto;
max-width: 920px;
}
.media-left {
width: 10% !important;
}
.media-body {
flex: 1;
padding-left: 0.3em;
}
.status .avatar {
width: 48px;
}
.status.compact .avatar {
width: 32px;
}
.status {
padding: 0.5em;
padding-right: 1em;
border-bottom: 1px solid silver;
}
.status-el:last-child .status {
border: none
}
[ng-click] {
cursor: pointer;
}
.status-el p {
margin: 0;
margin-top: 0.2em;
margin-bottom: 0.5em;
}
.user-info {
padding: 1em;
img {
border: 3px solid;
border-radius: 0.5em
}
.user-screen-name {
font-weight: lighter;
}
}
.user-counts {
display: flex;
padding: 1em 1em 0em 1em;
}
.user-count {
flex: 1;
h5 {
font-weight: lighter;
margin: 0;
}
span {
color: $main-color;
}
}
.fa {
- color: $main-color;
+ color: grey;
}
+
.status-actions {
width: 50%;
display: flex;
div, favorite-button {
flex: 1;
}
}
status-text-container {
display: block;
}
attention {
color: $main-color;
}
.status-el {
line-height: 18px;
.notify {
.avatar {
border-width: 3px;
border-color: $main-color;
border-style: solid;
}
}
.media-left {
img {
margin-top: 0.2em;
float: right;
margin-right: 0.3em;
border-radius: 20%;
}
}
.retweet-info {
padding: 0.3em;
.media-left {
display: flex;
i {
align-self: center;
text-align: right;
flex: 1;
padding-right: 0.3em;
}
}
}
.media-heading {
small {
font-weight: lighter;
}
}
}
nav {
z-index: 1000;
}
diff --git a/src/_variables.scss b/src/_variables.scss
new file mode 100644
index 0000000000..c317fd3240
--- /dev/null
+++ b/src/_variables.scss
@@ -0,0 +1,6 @@
+$main-color: #f58d2c;
+$main-background: white;
+$darkened-background: whitesmoke;
+$green: #0fa00f;
+$blue: #0095ff;
+
diff --git a/src/components/favorite_button/favorite_button.vue b/src/components/favorite_button/favorite_button.vue
index 0455c70603..6eaf0607b5 100644
--- a/src/components/favorite_button/favorite_button.vue
+++ b/src/components/favorite_button/favorite_button.vue
@@ -1,14 +1,21 @@
<template>
<div>
<i :class='classes' class='favorite-button fa' v-on:click.prevent='favorite()'></i>
<span v-if='status.fave_num > 0'>{{status.fave_num}}</span>
</div>
</template>
<script src="./favorite_button.js" ></script>
-<style>
- .favorite-button {
- cursor: pointer
- }
+<style lang='scss'>
+ @import '../../_variables.scss';
+ .favorite-button {
+ cursor: pointer;
+ &:hover {
+ color: $main-color;
+ }
+ }
+ .icon-star {
+ color: $main-color;
+ }
</style>
diff --git a/src/components/retweet_button/retweet_button.js b/src/components/retweet_button/retweet_button.js
new file mode 100644
index 0000000000..e7318dc5f7
--- /dev/null
+++ b/src/components/retweet_button/retweet_button.js
@@ -0,0 +1,19 @@
+const RetweetButton = {
+ props: [ 'status' ],
+ methods: {
+ retweet () {
+ if (!this.status.repeated) {
+ this.$store.dispatch('retweet', {id: this.status.id})
+ }
+ }
+ },
+ computed: {
+ classes () {
+ return {
+ 'retweeted': this.status.repeated
+ }
+ }
+ }
+}
+
+export default RetweetButton
diff --git a/src/components/retweet_button/retweet_button.vue b/src/components/retweet_button/retweet_button.vue
new file mode 100644
index 0000000000..9b2f5c7b36
--- /dev/null
+++ b/src/components/retweet_button/retweet_button.vue
@@ -0,0 +1,22 @@
+<template>
+ <div>
+ <i :class='classes' class='icon-retweet fa' v-on:click.prevent='retweet()'></i>
+ <span v-if='status.repeat_num > 0'>{{status.repeat_num}}</span>
+ </div>
+</template>
+
+<script src="./retweet_button.js" ></script>
+
+<style lang='scss'>
+ @import '../../_variables.scss';
+ .icon-retweet {
+ cursor: pointer;
+ &:hover {
+ color: $green;
+ }
+ }
+ .retweeted {
+ cursor: auto;
+ color: $green;
+ }
+</style>
diff --git a/src/components/status/status.js b/src/components/status/status.js
index 6253d334fe..0bf2ecdeee 100644
--- a/src/components/status/status.js
+++ b/src/components/status/status.js
@@ -1,36 +1,38 @@
import Attachment from '../attachment/attachment.vue'
import FavoriteButton from '../favorite_button/favorite_button.vue'
+import RetweetButton from '../retweet_button/retweet_button.vue'
import PostStatusForm from '../post_status_form/post_status_form.vue'
const Status = {
props: [ 'statusoid' ],
data: () => ({
replying: false
}),
computed: {
retweet () { return !!this.statusoid.retweeted_status },
retweeter () { return this.statusoid.user.name },
status () {
if (this.retweet) {
return this.statusoid.retweeted_status
} else {
return this.statusoid
}
},
loggedIn () {
return !!this.$store.state.users.currentUser
}
},
components: {
Attachment,
FavoriteButton,
+ RetweetButton,
PostStatusForm
},
methods: {
toggleReplying () {
this.replying = !this.replying
}
}
}
export default Status
diff --git a/src/components/status/status.vue b/src/components/status/status.vue
index 48c910c097..d4bcc27978 100644
--- a/src/components/status/status.vue
+++ b/src/components/status/status.vue
@@ -1,76 +1,79 @@
<template>
<div class="status-el">
<div v-if="retweet" class="media container retweet-info">
<div class="media-left">
<i class='fa icon-retweet'></i>
</div>
<div class="media-body">
Retweeted by {{retweeter}}
</div>
</div>
<div class="media status container" ng-class="{compact: compact, notify: notify}">
<div class="media-left">
<a :href="status.user.statusnet_profile_url">
<img class='avatar' :src="status.user.profile_image_url_original">
</a>
</div>
<div class="media-body">
<h4 class="media-heading">
<strong>{{status.user.name}}</strong>
<small><a :href="status.user.statusnet_profile_url">{{status.user.screen_name}}</a></small>
<small v-if="status.in_reply_to_screen_name"> &gt; <a :href="status.in_reply_to_profileurl">{{status.in_reply_to_screen_name}}</a></small>
-
<small>{{status.created_at_parsed}}</small>
</h4>
<div class="status-content" v-html="status.statusnet_html"></div>
<div v-if='status.attachments' class='attachments'>
<attachment :status-id="status.id" :nsfw="status.nsfw" :attachment="attachment" v-for="attachment in status.attachments">
</attachment>
</div>
<div v-if="loggedIn">
<div class='status-actions'>
<div>
<a href="#" v-on:click.prevent="toggleReplying">
<i class='fa icon-reply'></i>
</a>
</div>
- <div>
- <i class='fa icon-retweet'></i>
- </div>
+ <retweet-button :status=status></retweet-button>
<favorite-button :status=status></favorite-button>
</div>
<post-status-form v-if="replying" :reply-to="status.id" :attentions="status.attentions" :repliedUser="status.user" v-on:posted="toggleReplying"></post-status-form>
</div>
</div>
</div>
</div>
</template>
<script src="./status.js" ></script>
<style lang="scss">
- .status-el {
+ @import '../../_variables.scss';
+ .status-el {
hyphens: auto;
overflow-wrap: break-word;
word-wrap: break-word;
word-break: break-word;
a {
display: inline-block;
word-break: break-all;
}
.status-content {
margin-top: 3px;
margin-bottom: 3px;
}
- }
+ }
- .status-actions {
+ .status-actions {
padding-top: 5px;
- }
+ }
+
+ .icon-reply:hover {
+ color: $blue;
+ }
</style>
diff --git a/src/modules/statuses.js b/src/modules/statuses.js
index cc5e296cba..8238644d41 100644
--- a/src/modules/statuses.js
+++ b/src/modules/statuses.js
@@ -1,175 +1,184 @@
import { reduce, map, slice, last, intersectionBy, sortBy, unionBy, toInteger, groupBy, differenceBy, each, find } from 'lodash'
import moment from 'moment'
import apiService from '../services/api/api.service.js'
import parse from '../services/status_parser/status_parser.js'
export const defaultState = {
allStatuses: [],
maxId: 0,
timelines: {
public: {
statuses: [],
faves: [],
visibleStatuses: [],
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false
},
publicAndExternal: {
statuses: [],
faves: [],
visibleStatuses: [],
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false
},
friends: {
statuses: [],
faves: [],
visibleStatuses: [],
newStatusCount: 0,
maxId: 0,
minVisibleId: 0,
loading: false
}
}
}
const statusType = (status) => {
return !status.is_post_verb && status.uri.match(/fave/) ? 'fave' : 'status'
}
const addStatusesToTimeline = (addedStatuses, showImmediately, { statuses, visibleStatuses, newStatusCount, faves, loading }) => {
const statusesAndFaves = groupBy(addedStatuses, statusType)
const addedFaves = statusesAndFaves['fave'] || []
const unseenFaves = differenceBy(addedFaves, faves, 'id')
// Update fave count
each(unseenFaves, ({in_reply_to_status_id}) => {
const status = find(statuses, { id: toInteger(in_reply_to_status_id) })
if (status) {
status.fave_num += 1
}
})
addedStatuses = statusesAndFaves['status'] || []
// Add some html and nsfw to the statuses.
addedStatuses = map(addedStatuses, (status) => {
const statusoid = status.retweeted_status || status
statusoid.created_at_parsed = statusoid.created_at
statusoid.statusnet_html = parse(statusoid.statusnet_html)
if (statusoid.nsfw === undefined) {
const nsfwRegex = /#nsfw/i
statusoid.nsfw = statusoid.text.match(nsfwRegex)
}
return status
})
const newStatuses = sortBy(
unionBy(addedStatuses, statuses, 'id'),
({id}) => -id
)
let newNewStatusCount = newStatusCount + (newStatuses.length - statuses.length)
let newVisibleStatuses = visibleStatuses
if (showImmediately) {
newVisibleStatuses = unionBy(addedStatuses, newVisibleStatuses, 'id')
newVisibleStatuses = sortBy(newVisibleStatuses, ({id}) => -id)
newNewStatusCount = newStatusCount
};
newVisibleStatuses = intersectionBy(newStatuses, newVisibleStatuses, 'id')
return {
statuses: newStatuses,
visibleStatuses: newVisibleStatuses,
newStatusCount: newNewStatusCount,
maxId: newStatuses[0].id,
minVisibleId: (last(newVisibleStatuses) || { id: undefined }).id,
faves: unionBy(faves, addedFaves, 'id'),
loading
}
}
const updateTimestampsInStatuses = (statuses) => {
return map(statuses, (statusoid) => {
const status = statusoid.retweeted_status || statusoid
// Parse date
status.created_at_parsed = moment(status.created_at).fromNow()
return status
})
}
export const mutations = {
addNewStatuses (state, { statuses, showImmediately = false, timeline }) {
state.timelines[timeline] = addStatusesToTimeline(statuses, showImmediately, state.timelines[timeline])
state.allStatuses = unionBy(state.timelines[timeline].statuses, state.allStatuses, 'id')
// Set up retweets with most current status
const getRetweets = (result, status) => {
if (status.retweeted_status) {
result.push(status.retweeted_status)
}
return result
}
const retweets = reduce(statuses, getRetweets, [])
state.allStatuses = unionBy(retweets, state.allStatuses, 'id')
each(state.allStatuses, (status) => {
if (status.retweeted_status) {
const retweetedStatus = find(state.allStatuses, { id: status.retweeted_status.id })
status.retweeted_status = retweetedStatus
}
})
},
showNewStatuses (state, { timeline }) {
const oldTimeline = (state.timelines[timeline])
oldTimeline.newStatusCount = 0
oldTimeline.visibleStatuses = slice(oldTimeline.statuses, 0, 50)
},
updateTimestamps (state) {
updateTimestampsInStatuses(state.allStatuses)
},
setFavorited (state, { status, value }) {
const newStatus = find(state.allStatuses, status)
newStatus.favorited = value
},
+ setRetweeted (state, { status, value }) {
+ const newStatus = find(state.allStatuses, status)
+ newStatus.repeated = value
+ },
setLoading (state, { timeline, value }) {
state.timelines[timeline].loading = value
},
setNsfw (state, { id, nsfw }) {
const newStatus = find(state.allStatuses, { id })
newStatus.nsfw = nsfw
}
}
const statuses = {
state: defaultState,
actions: {
favorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: true })
apiService.favorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
},
unfavorite ({ rootState, commit }, status) {
// Optimistic favoriting...
commit('setFavorited', { status, value: false })
apiService.unfavorite({ id: status.id, credentials: rootState.users.currentUser.credentials })
+ },
+ retweet ({ rootState, commit }, status) {
+ // Optimistic retweeting...
+ commit('setRetweeted', { status, value: true })
+ apiService.retweet({ id: status.id, credentials: rootState.users.currentUser.credentials })
}
},
mutations
}
export default statuses
diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js
index 06585ac711..d00bc0d0ea 100644
--- a/src/services/api/api.service.js
+++ b/src/services/api/api.service.js
@@ -1,171 +1,180 @@
/* eslint-env browser */
const LOGIN_URL = '/api/account/verify_credentials.json'
const FRIENDS_TIMELINE_URL = '/api/statuses/friends_timeline.json'
const PUBLIC_TIMELINE_URL = '/api/statuses/public_timeline.json'
const PUBLIC_AND_EXTERNAL_TIMELINE_URL = '/api/statuses/public_and_external_timeline.json'
const FAVORITE_URL = '/api/favorites/create'
const UNFAVORITE_URL = '/api/favorites/destroy'
+const RETWEET_URL = '/api/statuses/retweet'
const STATUS_UPDATE_URL = '/api/statuses/update.json'
const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload'
// const CONVERSATION_URL = '/api/statusnet/conversation/';
// const FORM_CONTENT_TYPE = {'Content-Type': 'application/x-www-form-urlencoded'};
// import { param, ajax } from 'jquery';
// import { merge } from 'lodash';
const authHeaders = (user) => {
if (user) {
return { 'Authorization': `Basic ${btoa(`${user.username}:${user.password}`)}` }
} else {
return { }
}
}
const fetchTimeline = ({timeline, credentials, since = false, until = false}) => {
const timelineUrls = {
public: PUBLIC_TIMELINE_URL,
friends: FRIENDS_TIMELINE_URL,
'publicAndExternal': PUBLIC_AND_EXTERNAL_TIMELINE_URL
}
let url = timelineUrls[timeline]
if (since) {
url += `?since_id=${since}`
}
if (until) {
url += `?max_id=${until}`
}
return fetch(url, { headers: authHeaders(credentials) }).then((data) => data.json())
}
const verifyCredentials = (user) => {
return fetch(LOGIN_URL, {
method: 'POST',
headers: authHeaders(user)
})
}
const favorite = ({ id, credentials }) => {
return fetch(`${FAVORITE_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
const unfavorite = ({ id, credentials }) => {
return fetch(`${UNFAVORITE_URL}/${id}.json`, {
headers: authHeaders(credentials),
method: 'POST'
})
}
+const retweet = ({ id, credentials }) => {
+ return fetch(`${RETWEET_URL}/${id}.json`, {
+ headers: authHeaders(credentials),
+ method: 'POST'
+ })
+}
+
const postStatus = ({credentials, status, mediaIds, inReplyToStatusId}) => {
const idsText = mediaIds.join(',')
const form = new FormData()
form.append('status', status)
form.append('source', 'Pleroma FE')
form.append('media_ids', idsText)
if (inReplyToStatusId) {
form.append('in_reply_to_status_id', inReplyToStatusId)
}
return fetch(STATUS_UPDATE_URL, {
body: form,
method: 'POST',
headers: authHeaders(credentials)
})
}
const uploadMedia = ({formData, credentials}) => {
return fetch(MEDIA_UPLOAD_URL, {
body: formData,
method: 'POST',
headers: authHeaders(credentials)
})
.then((response) => response.text())
.then((text) => (new DOMParser()).parseFromString(text, 'application/xml'))
}
const apiService = {
verifyCredentials,
fetchTimeline,
favorite,
unfavorite,
+ retweet,
postStatus,
uploadMedia
}
export default apiService
// // TODO: This should probably be in redux.
// let authHeaders = {};
// const apiServiceFactory = ($http) => {
// // Public
// const fetchConversation = (id) => {
// return $http.get(`${CONVERSATION_URL}/${id}.json?count=100`);
// };
// const fetchTimeline = ({timeline, since = false, until = false}) => {
// const timelineUrls = {
// public: PUBLIC_TIMELINE_URL,
// friends: FRIENDS_TIMELINE_URL,
// 'public-and-external': PUBLIC_AND_EXTERNAL_TIMELINE_URL
// };
// let url = timelineUrls[timeline];
// if(since) {
// url += `?since_id=${since}`;
// }
// if(until) {
// url += `?max_id=${until}`;
// }
// return fetch(url, { headers: authHeaders }).then((data) => data.json());
// };
// // Need credentials
// const verifyCredentials = (user) => {
// const base64 = btoa(`${user.username}:${user.password}`);
// authHeaders = { "Authorization": `Basic ${base64}` };
// return $http.post(LOGIN_URL, null, { headers: authHeaders });
// };
// const unfavorite = (id) => $http.post(`${UNFAVORITE_URL}/${id}.json`, null, {headers: authHeaders});
// // This was impossible to get to work with $http. You're supposed to set Content-Type
// // undefined in the header so it sends the correct header. It would always send a json
// // content type. This method from jQuery worked right away...
// // Also, this method is only available as XML output. OLOLOLOLO
// const uploadMedia = (formData) => ajax({
// url: MEDIA_UPLOAD_URL,
// data: formData,
// type: 'POST',
// processData: false,
// contentType: false,
// headers: authHeaders
// });
// const apiService = {
// verifyCredentials,
// fetchConversation,
// postStatus,
// uploadMedia,
// favorite,
// unfavorite,
// fetchTimeline
// };
// return apiService;
// };
// apiServiceFactory.$inject = ['$http'];
// export default apiServiceFactory;

File Metadata

Mime Type
text/x-diff
Expires
Sat, Aug 8, 3:28 PM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1723655
Default Alt Text
(21 KB)

Event Timeline