Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F85628901
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Award Token
Flag For Later
Size
21 KB
Referenced Files
None
Subscribers
None
View Options
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"> > <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
Details
Attached
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)
Attached To
Mode
rPUFE pleroma-fe-upstream
Attached
Detach File
Event Timeline
Log In to Comment