From 3210283ae2783952c3724be188f2db8132885a38 Mon Sep 17 00:00:00 2001 From: dtluna Date: Tue, 22 Nov 2016 21:45:18 +0300 Subject: [PATCH 01/14] Add audio player --- src/components/attachment/attachment.js | 4 ++++ src/components/attachment/attachment.vue | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js index 99958589..47ca03de 100644 --- a/src/components/attachment/attachment.js +++ b/src/components/attachment/attachment.js @@ -23,6 +23,10 @@ const Attachment = { type = 'video' }; + if (this.attachment.mimetype.match(/ogg|audio/)) { + type = 'audio' + } + return type } }, diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 0e2a228a..4e8c1407 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -8,6 +8,8 @@ + + Don't know how to display this...
@@ -42,6 +44,10 @@ width: 100%; } + audio { + width: 100%; + } + img.media-upload { width: 100%; height: 100%; From 81c6f6e21f904e2889aa1b1d603f57c8f2772bfc Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:14:54 +0100 Subject: [PATCH 02/14] Remove example component. --- src/components/hello/Hello.css | 0 src/components/hello/Hello.html | 0 src/components/hello/Hello.js | 8 ------ src/components/hello/Hello.vue | 44 --------------------------------- 4 files changed, 52 deletions(-) delete mode 100644 src/components/hello/Hello.css delete mode 100644 src/components/hello/Hello.html delete mode 100644 src/components/hello/Hello.js delete mode 100644 src/components/hello/Hello.vue diff --git a/src/components/hello/Hello.css b/src/components/hello/Hello.css deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/hello/Hello.html b/src/components/hello/Hello.html deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/hello/Hello.js b/src/components/hello/Hello.js deleted file mode 100644 index c701c560..00000000 --- a/src/components/hello/Hello.js +++ /dev/null @@ -1,8 +0,0 @@ -export default { - name: 'hello', - data () { - return { - msg: 'Welcome to Your Vue.js app' - } - } -} diff --git a/src/components/hello/Hello.vue b/src/components/hello/Hello.vue deleted file mode 100644 index 828136a8..00000000 --- a/src/components/hello/Hello.vue +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - From a5f523922c1a93bbe781921e2d4bbdc8988436cc Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:15:34 +0100 Subject: [PATCH 03/14] Make timelineless status adding possible. --- src/modules/statuses.js | 32 ++++++++++++++++-------- test/unit/specs/modules/statuses.spec.js | 12 +++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index a3031b31..734ffc8a 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -1,4 +1,4 @@ -import { remove, map, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max } from 'lodash' +import { remove, map, slice, sortBy, toInteger, each, find, flatten, maxBy, last, merge, max, isArray } from 'lodash' import moment from 'moment' import apiService from '../services/api/api.service.js' // import parse from '../services/status_parser/status_parser.js' @@ -100,11 +100,17 @@ const mergeOrAdd = (arr, item) => { } const addNewStatuses = (state, { statuses, showImmediately = false, timeline, user = {} }) => { + // Sanity check + if (!isArray(statuses)) { + return false + } + const allStatuses = state.allStatuses const timelineObject = state.timelines[timeline] // Set the maxId to the new id if it's larger. const updateMaxId = ({id}) => { + if (!timeline) { return false } timelineObject.maxId = max([id, timelineObject.maxId]) } @@ -117,15 +123,15 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us } // Some statuses should only be added to the global status repository. - if (addToTimeline) { + if (timeline && addToTimeline) { mergeOrAdd(timelineObject.statuses, status) } - if (showImmediately) { + if (timeline && showImmediately) { // Add it directly to the visibleStatuses, don't change // newStatusCount mergeOrAdd(timelineObject.visibleStatuses, status) - } else if (addToTimeline && result.new) { + } else if (timeline && addToTimeline && result.new) { // Just change newStatuscount timelineObject.newStatusCount += 1 } @@ -159,7 +165,7 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us let retweet // If the retweeted status is already there, don't add the retweet // to the timeline. - if (find(timelineObject.visibleStatuses, {id: retweetedStatus.id})) { + if (timeline && find(timelineObject.visibleStatuses, {id: retweetedStatus.id})) { // Already have it visible, don't add to timeline, don't show. retweet = addStatus(status, false, false) } else { @@ -177,8 +183,10 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us updateMaxId(deletion) remove(allStatuses, { uri }) - remove(timelineObject.statuses, { uri }) - remove(timelineObject.visibleStatuses, { uri }) + if (timeline) { + remove(timelineObject.statuses, { uri }) + remove(timelineObject.visibleStatuses, { uri }) + } }, 'default': (unknown) => { console.log(unknown) @@ -192,9 +200,11 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us }) // Keep the visible statuses sorted - timelineObject.visibleStatuses = sortBy(timelineObject.visibleStatuses, ({id}) => -id) - timelineObject.statuses = sortBy(timelineObject.statuses, ({id}) => -id) - timelineObject.minVisibleId = (last(timelineObject.statuses) || {}).id + if (timeline) { + timelineObject.visibleStatuses = sortBy(timelineObject.visibleStatuses, ({id}) => -id) + timelineObject.statuses = sortBy(timelineObject.statuses, ({id}) => -id) + timelineObject.minVisibleId = (last(timelineObject.statuses) || {}).id + } } export const mutations = { @@ -228,7 +238,7 @@ export const mutations = { const statuses = { state: defaultState, actions: { - addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline }) { + addNewStatuses ({ rootState, commit }, { statuses, showImmediately = false, timeline = false }) { commit('addNewStatuses', { statuses, showImmediately, timeline, user: rootState.users.currentUser }) }, favorite ({ rootState, commit }, status) { diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index 574e4f74..a50e4f9c 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -67,6 +67,18 @@ describe('The Statuses module', () => { expect(state.timelines.public.newStatusCount).to.equal(1) }) + it('add the statuses to allStatuses if no timeline is given', () => { + const state = cloneDeep(defaultState) + const status = makeMockStatus({id: 1}) + + mutations.addNewStatuses(state, { statuses: [status] }) + + expect(state.allStatuses).to.eql([status]) + expect(state.timelines.public.statuses).to.eql([]) + expect(state.timelines.public.visibleStatuses).to.eql([]) + expect(state.timelines.public.newStatusCount).to.equal(0) + }) + it('adds the status to allStatuses and to the given timeline, directly visible', () => { const state = cloneDeep(defaultState) const status = makeMockStatus({id: 1}) From 18c11e405a343d542d63b4bfd09a3388f9f6b0d8 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:16:20 +0100 Subject: [PATCH 04/14] Add status and conversation fetching to apiService. --- src/services/api/api.service.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/services/api/api.service.js b/src/services/api/api.service.js index d828aff0..87102376 100644 --- a/src/services/api/api.service.js +++ b/src/services/api/api.service.js @@ -7,18 +7,16 @@ 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 STATUS_URL = '/api/statuses/show' const MEDIA_UPLOAD_URL = '/api/statusnet/media/upload' -// const CONVERSATION_URL = '/api/statusnet/conversation/'; +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 oldfetch = window.fetch let fetch = (url, options) => { const baseUrl = '' const fullUrl = baseUrl + url - return window.fetch(fullUrl, options) + return oldfetch(fullUrl, options) } const authHeaders = (user) => { @@ -29,6 +27,16 @@ const authHeaders = (user) => { } } +const fetchConversation = ({id}) => { + let url = `${CONVERSATION_URL}/${id}.json?count=100` + return fetch(url).then((data) => data.json()) +} + +const fetchStatus = ({id}) => { + let url = `${STATUS_URL}/${id}.json` + return fetch(url).then((data) => data.json()) +} + const fetchTimeline = ({timeline, credentials, since = false, until = false}) => { const timelineUrls = { public: PUBLIC_TIMELINE_URL, @@ -108,6 +116,8 @@ const uploadMedia = ({formData, credentials}) => { const apiService = { verifyCredentials, fetchTimeline, + fetchConversation, + fetchStatus, favorite, unfavorite, retweet, From e245074ef70e0a98c79168cb7e04390dc8d5e97c Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:17:09 +0100 Subject: [PATCH 05/14] Add status / conversation component. And wire it up. --- src/components/conversation/conversation.js | 48 ++++++++++++++++++++ src/components/conversation/conversation.vue | 12 +++++ src/main.js | 4 +- 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/components/conversation/conversation.js create mode 100644 src/components/conversation/conversation.vue diff --git a/src/components/conversation/conversation.js b/src/components/conversation/conversation.js new file mode 100644 index 00000000..ea26d958 --- /dev/null +++ b/src/components/conversation/conversation.js @@ -0,0 +1,48 @@ +import { find, filter, sortBy, toInteger } from 'lodash' +import Status from '../status/status.vue' +import apiService from '../../services/api/api.service.js' + +const conversation = { + computed: { + status () { + const id = toInteger(this.$route.params.id) + const statuses = this.$store.state.statuses.allStatuses + const status = find(statuses, {id}) + + return status + }, + conversation () { + if (!this.status) { + return false + } + + const conversationId = this.status.statusnet_conversation_id + const statuses = this.$store.state.statuses.allStatuses + const conversation = filter(statuses, { statusnet_conversation_id: conversationId }) + return sortBy(conversation, 'id') + } + }, + components: { + Status + }, + created () { + this.fetchConversation() + }, + methods: { + fetchConversation () { + if (this.status) { + const conversationId = this.status.statusnet_conversation_id + apiService.fetchConversation({id: conversationId}) + .then((statuses) => this.$store.dispatch('addNewStatuses', { statuses })) + .then(() => this.$store.commit('updateTimestamps')) + } else { + const id = this.$route.params.id + apiService.fetchStatus({id}) + .then((status) => this.$store.dispatch('addNewStatuses', { statuses: [status] })) + .then(() => this.fetchConversation()) + } + } + } +} + +export default conversation diff --git a/src/components/conversation/conversation.vue b/src/components/conversation/conversation.vue new file mode 100644 index 00000000..60b3f044 --- /dev/null +++ b/src/components/conversation/conversation.vue @@ -0,0 +1,12 @@ + + + diff --git a/src/main.js b/src/main.js index de3b2af1..64d331f1 100644 --- a/src/main.js +++ b/src/main.js @@ -5,6 +5,7 @@ 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 Conversation from './components/conversation/conversation.vue' import statusesModule from './modules/statuses.js' import usersModule from './modules/users.js' @@ -23,7 +24,8 @@ const routes = [ { path: '/', redirect: '/main/all' }, { path: '/main/all', component: PublicAndExternalTimeline }, { path: '/main/public', component: PublicTimeline }, - { path: '/main/friends', component: FriendsTimeline } + { path: '/main/friends', component: FriendsTimeline }, + { name: 'conversation', path: '/notice/:id', component: Conversation } ] const router = new VueRouter({ From 2341a3692a31f40c484a3b027f757c84551bf1fe Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:19:01 +0100 Subject: [PATCH 06/14] Add link to conversation in status. --- src/components/status/status.vue | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index d4bcc279..a84917e6 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -20,7 +20,11 @@ {{status.user.screen_name}} > {{status.in_reply_to_screen_name}} - - {{status.created_at_parsed}} + + + {{status.created_at_parsed}} + +
From ce0071d6e085ada533810d73200607381b9a5807 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:24:35 +0100 Subject: [PATCH 07/14] Remember positions on scroll. --- src/main.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.js b/src/main.js index 64d331f1..3d3ef1b4 100644 --- a/src/main.js +++ b/src/main.js @@ -30,7 +30,10 @@ const routes = [ const router = new VueRouter({ mode: 'history', - routes + routes, + scrollBehavior: (to, from, savedPosition) => { + return savedPosition || { x: 0, y: 0 } + } }) /* eslint-disable no-new */ From 4f0155c5eb532fea6cde2aea870824f936923132 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 18:31:18 +0100 Subject: [PATCH 08/14] Timeline status adding fixes. Don't show new statuses immediately if we already have something in there. --- src/components/timeline/timeline.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/timeline/timeline.js b/src/components/timeline/timeline.js index 4ebc383f..8799e69c 100644 --- a/src/components/timeline/timeline.js +++ b/src/components/timeline/timeline.js @@ -12,12 +12,13 @@ const Timeline = { created () { const store = this.$store const credentials = store.state.users.currentUser.credentials + const showImmediately = this.timeline.visibleStatuses.length === 0 timelineFetcher.fetchAndUpdate({ store, credentials, timeline: this.timelineName, - showImmediately: true + showImmediately }) }, methods: { From c8d25eab61450340039374f0ffc4b2388825714b Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Thu, 24 Nov 2016 20:49:30 +0100 Subject: [PATCH 09/14] Image CSS fix for Firefox. --- src/components/attachment/attachment.vue | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/attachment/attachment.vue b/src/components/attachment/attachment.vue index 4e8c1407..d5578c04 100644 --- a/src/components/attachment/attachment.vue +++ b/src/components/attachment/attachment.vue @@ -97,7 +97,6 @@ img { width: 100%; - flex: 1; border: 1px solid; border-radius: 0.5em; width: 100%; From 08393b8580385d2627da2ebd9e3a09455d101a3a Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Fri, 25 Nov 2016 13:42:33 +0100 Subject: [PATCH 10/14] Fix word wrapping on Firefox. Also, move some css around. See https://bugzilla.mozilla.org/show_bug.cgi?id=1136818 for the word wrapping stuff. --- src/App.scss | 28 ++-------------------------- src/components/status/status.vue | 24 ++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/src/App.scss b/src/App.scss index a97ad56d..47886e31 100644 --- a/src/App.scss +++ b/src/App.scss @@ -145,10 +145,6 @@ status.ng-enter.ng-enter-active { } -.media-body { - flex: 1 -} - #content { margin: auto; max-width: 920px; @@ -163,34 +159,14 @@ status.ng-enter.ng-enter-active { 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 +.container > * { + min-width: 0px; } [ng-click] { cursor: pointer; } -.status-el p { - margin: 0; - margin-top: 0.2em; - margin-bottom: 0.5em; -} - .user-info { padding: 1em; img { diff --git a/src/components/status/status.vue b/src/components/status/status.vue index a84917e6..e74be215 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -71,6 +71,12 @@ margin-top: 3px; margin-bottom: 3px; } + + p { + margin: 0; + margin-top: 0.2em; + margin-bottom: 0.5em; + } } .status-actions { @@ -80,4 +86,22 @@ .icon-reply:hover { color: $blue; } + + .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 + } From 1d8c8131352d05d796a6baf65a09fbdd1726fedf Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Fri, 25 Nov 2016 16:56:08 +0100 Subject: [PATCH 11/14] Better handling of favorites. --- src/modules/statuses.js | 16 ++++++++++-- test/unit/specs/modules/statuses.spec.js | 33 ++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/src/modules/statuses.js b/src/modules/statuses.js index 734ffc8a..37115506 100644 --- a/src/modules/statuses.js +++ b/src/modules/statuses.js @@ -7,6 +7,7 @@ export const defaultState = { allStatuses: [], maxId: 0, notifications: [], + favorites: new Set(), timelines: { public: { statuses: [], @@ -147,6 +148,13 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us const status = find(allStatuses, { id: toInteger(favorite.in_reply_to_status_id) }) if (status) { status.fave_num += 1 + + // This is our favorite, so the relevant bit. + if (favorite.user.id === user.id) { + status.favorited = true + } + + // Add a notification if the user's status is favorited if (status.user.id === user.id) { addNotification({type: 'favorite', status, action: favorite}) } @@ -175,8 +183,12 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us retweet.retweeted_status = retweetedStatus }, 'favorite': (favorite) => { - updateMaxId(favorite) - favoriteStatus(favorite) + // Only update if this is a new favorite. + if (!state.favorites.has(favorite.id)) { + state.favorites.add(favorite.id) + updateMaxId(favorite) + favoriteStatus(favorite) + } }, 'deletion': (deletion) => { const uri = deletion.uri diff --git a/test/unit/specs/modules/statuses.spec.js b/test/unit/specs/modules/statuses.spec.js index a50e4f9c..f068bb92 100644 --- a/test/unit/specs/modules/statuses.spec.js +++ b/test/unit/specs/modules/statuses.spec.js @@ -197,7 +197,8 @@ describe('The Statuses module', () => { is_post_verb: false, in_reply_to_status_id: '1', // The API uses strings here... uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', - text: 'a favorited something by b' + text: 'a favorited something by b', + user: {} } mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public' }) @@ -206,6 +207,33 @@ describe('The Statuses module', () => { expect(state.timelines.public.visibleStatuses.length).to.eql(1) expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) expect(state.timelines.public.maxId).to.eq(favorite.id) + + // Adding it again does nothing + mutations.addNewStatuses(state, { statuses: [favorite], showImmediately: true, timeline: 'public' }) + + expect(state.timelines.public.visibleStatuses.length).to.eql(1) + expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(1) + expect(state.timelines.public.maxId).to.eq(favorite.id) + + // If something is favorited by the current user, it also sets the 'favorited' property + const user = { + id: 1 + } + + const ownFavorite = { + id: 3, + is_post_verb: false, + in_reply_to_status_id: '1', // The API uses strings here... + uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', + text: 'a favorited something by b', + user + } + + mutations.addNewStatuses(state, { statuses: [ownFavorite], showImmediately: true, timeline: 'public', user }) + + expect(state.timelines.public.visibleStatuses.length).to.eql(1) + expect(state.timelines.public.visibleStatuses[0].fave_num).to.eql(2) + expect(state.timelines.public.visibleStatuses[0].favorited).to.eql(true) }) describe('notifications', () => { @@ -220,7 +248,8 @@ describe('The Statuses module', () => { is_post_verb: false, in_reply_to_status_id: '1', // The API uses strings here... uri: 'tag:shitposter.club,2016-08-21:fave:3895:note:773501:2016-08-21T16:52:15+00:00', - text: 'a favorited something by b' + text: 'a favorited something by b', + user: {} } mutations.addNewStatuses(state, { statuses: [status], showImmediately: true, timeline: 'public', user }) From 5986afbb201e2a42bd0954c35e284c932b2e993c Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Fri, 25 Nov 2016 17:19:46 +0100 Subject: [PATCH 12/14] Re-indent style. --- src/components/status/status.vue | 48 ++++++++++++++++---------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index e74be215..b00f97ca 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -55,8 +55,8 @@ From b515586485fab7030e02cad3cf1ce2c6e451c7d9 Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Fri, 25 Nov 2016 17:34:41 +0100 Subject: [PATCH 13/14] Show link to status source url. --- src/components/status/status.vue | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index b00f97ca..14792cae 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -25,6 +25,9 @@ {{status.created_at_parsed}} + + Source +
@@ -62,6 +65,10 @@ word-wrap: break-word; word-break: break-word; + .source_url { + float: right; + } + a { display: inline-block; word-break: break-all; From 1be1d7563c94fa961c1cc0cef03e7e4e69df178a Mon Sep 17 00:00:00 2001 From: Roger Braun Date: Fri, 25 Nov 2016 17:34:59 +0100 Subject: [PATCH 14/14] Greentext. --- src/components/status/status.vue | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/status/status.vue b/src/components/status/status.vue index 14792cae..9d17b8a7 100644 --- a/src/components/status/status.vue +++ b/src/components/status/status.vue @@ -69,6 +69,10 @@ float: right; } + .greentext { + color: green; + } + a { display: inline-block; word-break: break-all;