editing #158
16 changed files with 365 additions and 14 deletions
|
@ -10,3 +10,4 @@ Contributors of this project.
|
|||
- shpuld (shpuld@shitposter.club): CSS and styling
|
||||
- Vincent Guth (https://unsplash.com/photos/XrwVIFy6rTw): Background images.
|
||||
- hj (hj@shigusegubu.club): Code
|
||||
- Sean King (seanking@freespeechextremist.com): Code
|
||||
|
|
|
@ -10,6 +10,7 @@ import MobilePostStatusButton from './components/mobile_post_status_button/mobil
|
|||
import MobileNav from './components/mobile_nav/mobile_nav.vue'
|
||||
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
|
||||
import UserReportingModal from './components/user_reporting_modal/user_reporting_modal.vue'
|
||||
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
|
||||
import PostStatusModal from './components/post_status_modal/post_status_modal.vue'
|
||||
import GlobalNoticeList from './components/global_notice_list/global_notice_list.vue'
|
||||
import { windowWidth, windowHeight } from './services/window_utils/window_utils'
|
||||
|
@ -33,6 +34,7 @@ export default {
|
|||
SettingsModal,
|
||||
UserReportingModal,
|
||||
PostStatusModal,
|
||||
EditStatusModal,
|
||||
GlobalNoticeList
|
||||
},
|
||||
data: () => ({
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
<MobilePostStatusButton />
|
||||
<UserReportingModal />
|
||||
<PostStatusModal />
|
||||
<EditStatusModal />
|
||||
<SettingsModal />
|
||||
<UpdateNotification />
|
||||
<GlobalNoticeList />
|
||||
|
|
75
src/components/edit_status_modal/edit_status_modal.js
Normal file
75
src/components/edit_status_modal/edit_status_modal.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
import PostStatusForm from '../post_status_form/post_status_form.vue'
|
||||
import Modal from '../modal/modal.vue'
|
||||
import statusPosterService from '../../services/status_poster/status_poster.service.js'
|
||||
import get from 'lodash/get'
|
||||
|
||||
const EditStatusModal = {
|
||||
components: {
|
||||
PostStatusForm,
|
||||
Modal
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
resettingForm: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn () {
|
||||
return !!this.$store.state.users.currentUser
|
||||
},
|
||||
modalActivated () {
|
||||
return this.$store.state.editStatus.modalActivated
|
||||
},
|
||||
isFormVisible () {
|
||||
return this.isLoggedIn && !this.resettingForm && this.modalActivated
|
||||
},
|
||||
params () {
|
||||
return this.$store.state.editStatus.params || {}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
params (newVal, oldVal) {
|
||||
if (get(newVal, 'repliedUser.id') !== get(oldVal, 'repliedUser.id')) {
|
||||
this.resettingForm = true
|
||||
this.$nextTick(() => {
|
||||
this.resettingForm = false
|
||||
})
|
||||
}
|
||||
},
|
||||
isFormVisible (val) {
|
||||
if (val) {
|
||||
this.$nextTick(() => this.$el && this.$el.querySelector('textarea').focus())
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
doEditStatus ({ status, spoilerText, sensitive, media, contentType, poll }) {
|
||||
const params = {
|
||||
store: this.$store,
|
||||
statusId: this.$store.state.editStatus.params.statusId,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
media,
|
||||
contentType
|
||||
}
|
||||
|
||||
return statusPosterService.editStatus(params)
|
||||
.then((data) => {
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error editing status', err)
|
||||
return {
|
||||
error: err.message
|
||||
}
|
||||
})
|
||||
},
|
||||
closeModal () {
|
||||
this.$store.dispatch('closeEditStatusModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default EditStatusModal
|
48
src/components/edit_status_modal/edit_status_modal.vue
Normal file
48
src/components/edit_status_modal/edit_status_modal.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<Modal
|
||||
v-if="isFormVisible"
|
||||
class="edit-form-modal-view"
|
||||
@backdropClicked="closeModal"
|
||||
>
|
||||
<div class="edit-form-modal-panel panel">
|
||||
<div class="panel-heading">
|
||||
{{ $t('post_status.edit_status') }}
|
||||
</div>
|
||||
<PostStatusForm
|
||||
class="panel-body"
|
||||
v-bind="params"
|
||||
@posted="closeModal"
|
||||
:disablePolls="true"
|
||||
:disableVisibilitySelector="true"
|
||||
:post-handler="doEditStatus"
|
||||
/>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script src="./edit_status_modal.js"></script>
|
||||
|
||||
<style lang="scss">
|
||||
.modal-view.edit-form-modal-view {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.edit-form-modal-panel {
|
||||
flex-shrink: 0;
|
||||
margin-top: 25%;
|
||||
margin-bottom: 2em;
|
||||
width: 100%;
|
||||
max-width: 700px;
|
||||
|
||||
@media (orientation: landscape) {
|
||||
margin-top: 8%;
|
||||
}
|
||||
|
||||
.form-bottom-left {
|
||||
max-width: 6.5em;
|
||||
|
||||
.emoji-icon {
|
||||
justify-content: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -101,6 +101,19 @@ const ExtraButtons = {
|
|||
},
|
||||
reportStatus () {
|
||||
this.$store.dispatch('openUserReportingModal', { userId: this.status.user.id, statusIds: [this.status.id] })
|
||||
},
|
||||
editStatus () {
|
||||
this.$store.dispatch('fetchStatusSource', { id: this.status.id })
|
||||
.then(data => this.$store.dispatch('openEditStatusModal', {
|
||||
statusId: this.status.id,
|
||||
subject: data.spoiler_text,
|
||||
statusText: data.text,
|
||||
statusIsSensitive: this.status.nsfw,
|
||||
statusPoll: this.status.poll,
|
||||
statusFiles: this.status.attachments,
|
||||
visibility: this.status.visibility,
|
||||
statusContentType: data.content_type
|
||||
}))
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
|
@ -73,6 +73,17 @@
|
|||
icon="bookmark"
|
||||
/><span>{{ $t("status.unbookmark") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="ownStatus"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
@click.prevent="editStatus"
|
||||
@click="close"
|
||||
>
|
||||
<FAIcon
|
||||
fixed-width
|
||||
icon="pen"
|
||||
/><span>{{ $t("status.edit") }}</span>
|
||||
</button>
|
||||
<button
|
||||
v-if="canDelete"
|
||||
class="button-default dropdown-item dropdown-item-icon"
|
||||
|
|
|
@ -55,6 +55,14 @@ const pxStringToNumber = (str) => {
|
|||
|
||||
const PostStatusForm = {
|
||||
props: [
|
||||
'statusId',
|
||||
'statusText',
|
||||
'statusIsSensitive',
|
||||
'statusPoll',
|
||||
'statusFiles',
|
||||
'statusMediaDescriptions',
|
||||
'statusScope',
|
||||
'statusContentType',
|
||||
'replyTo',
|
||||
'quoteId',
|
||||
'repliedUser',
|
||||
|
@ -63,6 +71,7 @@ const PostStatusForm = {
|
|||
'subject',
|
||||
'disableSubject',
|
||||
'disableScopeSelector',
|
||||
'disableVisibilitySelector',
|
||||
'disableNotice',
|
||||
'disableLockWarning',
|
||||
'disablePolls',
|
||||
|
@ -120,23 +129,40 @@ const PostStatusForm = {
|
|||
|
||||
const { postContentType: contentType, sensitiveByDefault, sensitiveIfSubject } = this.$store.getters.mergedConfig
|
||||
|
||||
let statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
status: statusText,
|
||||
sensitiveByDefault,
|
||||
nsfw: !!sensitiveByDefault,
|
||||
files: [],
|
||||
poll: {},
|
||||
mediaDescriptions: {},
|
||||
visibility: this.suggestedVisibility(),
|
||||
contentType
|
||||
}
|
||||
|
||||
if (this.statusId) {
|
||||
const statusContentType = this.statusContentType || contentType
|
||||
statusParams = {
|
||||
spoilerText: this.subject || '',
|
||||
status: this.statusText || '',
|
||||
sensitiveIfSubject,
|
||||
nsfw: this.statusIsSensitive || !!sensitiveByDefault,
|
||||
files: this.statusFiles || [],
|
||||
poll: this.statusPoll || {},
|
||||
mediaDescriptions: this.statusMediaDescriptions || {},
|
||||
visibility: this.statusScope || this.suggestedVisibility(),
|
||||
contentType: statusContentType
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dropFiles: [],
|
||||
uploadingFiles: false,
|
||||
error: null,
|
||||
posting: false,
|
||||
highlighted: 0,
|
||||
newStatus: {
|
||||
spoilerText: this.subject || '',
|
||||
status: statusText,
|
||||
sensitiveIfSubject,
|
||||
nsfw: !!sensitiveByDefault,
|
||||
files: [],
|
||||
poll: {},
|
||||
mediaDescriptions: {},
|
||||
visibility: this.suggestedVisibility(),
|
||||
contentType
|
||||
},
|
||||
newStatus: statusParams,
|
||||
caret: 0,
|
||||
pollFormVisible: false,
|
||||
showDropIcon: 'hide',
|
||||
|
|
|
@ -180,6 +180,7 @@
|
|||
class="visibility-tray"
|
||||
>
|
||||
<scope-selector
|
||||
v-if="!disableVisibilitySelector"
|
||||
:show-all="showAllScopes"
|
||||
:user-default="userDefaultScope"
|
||||
:original-scope="copyMessageScope"
|
||||
|
|
|
@ -19,6 +19,7 @@ import reportsModule from './modules/reports.js'
|
|||
import pollsModule from './modules/polls.js'
|
||||
import postStatusModule from './modules/postStatus.js'
|
||||
import announcementsModule from './modules/announcements.js'
|
||||
import editStatusModule from './modules/editStatus.js'
|
||||
|
||||
import { createI18n } from 'vue-i18n'
|
||||
|
||||
|
@ -81,7 +82,8 @@ const persistedStateOptions = {
|
|||
reports: reportsModule,
|
||||
polls: pollsModule,
|
||||
postStatus: postStatusModule,
|
||||
announcements: announcementsModule
|
||||
announcements: announcementsModule,
|
||||
editStatus: editStatusModule
|
||||
},
|
||||
plugins,
|
||||
strict: false // Socket modifies itself, let's ignore this for now.
|
||||
|
|
|
@ -98,6 +98,8 @@ const api = {
|
|||
showImmediately: timelineData.visibleStatuses.length === 0,
|
||||
timeline: 'friends'
|
||||
})
|
||||
} else if (message.event === 'status.update') {
|
||||
// Insert dispatch code here.
|
||||
} else if (message.event === 'delete') {
|
||||
dispatch('deleteStatusById', message.id)
|
||||
}
|
||||
|
|
25
src/modules/editStatus.js
Normal file
25
src/modules/editStatus.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
const editStatus = {
|
||||
state: {
|
||||
params: null,
|
||||
modalActivated: false
|
||||
},
|
||||
mutations: {
|
||||
openEditStatusModal (state, params) {
|
||||
state.params = params
|
||||
state.modalActivated = true
|
||||
},
|
||||
closeEditStatusModal (state) {
|
||||
state.modalActivated = false
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
openEditStatusModal ({ commit }, params) {
|
||||
commit('openEditStatusModal', params)
|
||||
},
|
||||
closeEditStatusModal ({ commit }) {
|
||||
commit('closeEditStatusModal')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default editStatus
|
|
@ -251,6 +251,9 @@ const addNewStatuses = (state, { statuses, showImmediately = false, timeline, us
|
|||
'status': (status) => {
|
||||
addStatus(status, showImmediately)
|
||||
},
|
||||
'edit': (status) => {
|
||||
addStatus(status, showImmediately)
|
||||
},
|
||||
'retweet': (status) => {
|
||||
// RetweetedStatuses are never shown immediately
|
||||
const retweetedStatus = addStatus(status.retweeted_status, false, false)
|
||||
|
@ -607,6 +610,9 @@ const statuses = {
|
|||
return rootState.api.backendInteractor.fetchStatus({ id })
|
||||
.then((status) => dispatch('addNewStatuses', { statuses: [status] }))
|
||||
},
|
||||
fetchStatusSource ({ rootState, dispatch }, status) {
|
||||
return apiService.fetchStatusSource({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
},
|
||||
deleteStatus ({ rootState, commit }, status) {
|
||||
commit('setDeleted', { status })
|
||||
apiService.deleteStatus({ id: status.id, credentials: rootState.users.currentUser.credentials })
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { each, map, concat, last, get } from 'lodash'
|
||||
import { parseStatus, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import { parseStatus, parseSource, parseUser, parseNotification, parseAttachment, parseLinkHeaderPagination } from '../entity_normalizer/entity_normalizer.service.js'
|
||||
import { RegistrationError, StatusCodeError } from '../errors/errors'
|
||||
|
||||
/* eslint-env browser */
|
||||
|
@ -52,6 +52,8 @@ const MASTODON_USER_HOME_TIMELINE_URL = '/api/v1/timelines/home'
|
|||
const AKKOMA_BUBBLE_TIMELINE_URL = '/api/v1/timelines/bubble'
|
||||
const MASTODON_STATUS_URL = id => `/api/v1/statuses/${id}`
|
||||
const MASTODON_STATUS_CONTEXT_URL = id => `/api/v1/statuses/${id}/context`
|
||||
const MASTODON_STATUS_SOURCE_URL = id => `/api/v1/statuses/${id}/source`
|
||||
const MASTODON_STATUS_HISTORY_URL = id => `/api/v1/statuses/${id}/history`
|
||||
const MASTODON_USER_URL = id => `/api/v1/accounts/${id}?with_relationships=true`
|
||||
const MASTODON_USER_RELATIONSHIPS_URL = '/api/v1/accounts/relationships'
|
||||
const MASTODON_USER_TIMELINE_URL = id => `/api/v1/accounts/${id}/statuses`
|
||||
|
@ -512,6 +514,32 @@ const fetchStatus = ({ id, credentials }) => {
|
|||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const fetchStatusSource = ({ id, credentials }) => {
|
||||
let url = MASTODON_STATUS_SOURCE_URL(id)
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => {
|
||||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching source', data)
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => parseSource(data))
|
||||
}
|
||||
|
||||
const fetchStatusHistory = ({ id, credentials }) => {
|
||||
let url = MASTODON_STATUS_HISTORY_URL(id)
|
||||
return fetch(url, { headers: authHeaders(credentials) })
|
||||
.then((data) => {
|
||||
if (data.ok) {
|
||||
return data
|
||||
}
|
||||
throw new Error('Error fetching history', data)
|
||||
})
|
||||
.then((data) => data.json())
|
||||
.then((data) => parseStatus(data))
|
||||
}
|
||||
|
||||
const tagUser = ({ tag, credentials, user }) => {
|
||||
const screenName = user.screen_name
|
||||
const form = {
|
||||
|
@ -837,6 +865,54 @@ const postStatus = ({
|
|||
.then((data) => data.error ? data : parseStatus(data))
|
||||
}
|
||||
|
||||
const editStatus = ({
|
||||
id,
|
||||
credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds = [],
|
||||
contentType
|
||||
}) => {
|
||||
const form = new FormData()
|
||||
const pollOptions = poll.options || []
|
||||
|
||||
form.append('status', status)
|
||||
if (spoilerText) form.append('spoiler_text', spoilerText)
|
||||
if (sensitive) form.append('sensitive', sensitive)
|
||||
if (contentType) form.append('content_type', contentType)
|
||||
mediaIds.forEach(val => {
|
||||
form.append('media_ids[]', val)
|
||||
})
|
||||
|
||||
if (pollOptions.some(option => option !== '')) {
|
||||
const normalizedPoll = {
|
||||
expires_in: poll.expiresIn,
|
||||
multiple: poll.multiple
|
||||
}
|
||||
Object.keys(normalizedPoll).forEach(key => {
|
||||
form.append(`poll[${key}]`, normalizedPoll[key])
|
||||
})
|
||||
|
||||
pollOptions.forEach(option => {
|
||||
form.append('poll[options][]', option)
|
||||
})
|
||||
}
|
||||
|
||||
let putHeaders = authHeaders(credentials)
|
||||
|
||||
return fetch(MASTODON_STATUS_URL(id), {
|
||||
body: form,
|
||||
method: 'PUT',
|
||||
headers: putHeaders
|
||||
})
|
||||
.then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
.then((data) => data.error ? data : parseStatus(data))
|
||||
}
|
||||
|
||||
const deleteStatus = ({ id, credentials }) => {
|
||||
return fetch(MASTODON_DELETE_URL(id), {
|
||||
headers: authHeaders(credentials),
|
||||
|
@ -1393,9 +1469,12 @@ const MASTODON_STREAMING_EVENTS = new Set([
|
|||
'update',
|
||||
'notification',
|
||||
'delete',
|
||||
'filters_changed'
|
||||
'filters_changed',
|
||||
'status.update'
|
||||
])
|
||||
|
||||
// If Mastodon is doing a different streaming event,
|
||||
// please let us know, Gargron.
|
||||
const PLEROMA_STREAMING_EVENTS = new Set([
|
||||
])
|
||||
|
||||
|
@ -1474,6 +1553,8 @@ export const handleMastoWS = (wsEvent) => {
|
|||
const data = payload ? JSON.parse(payload) : null
|
||||
if (event === 'update') {
|
||||
return { event, status: parseStatus(data) }
|
||||
} else if (event === 'status.update') {
|
||||
return { event, status: parseStatus(data) }
|
||||
} else if (event === 'notification') {
|
||||
return { event, notification: parseNotification(data) }
|
||||
}
|
||||
|
@ -1498,6 +1579,8 @@ const apiService = {
|
|||
fetchPinnedStatuses,
|
||||
fetchConversation,
|
||||
fetchStatus,
|
||||
fetchStatusSource,
|
||||
fetchStatusHistory,
|
||||
fetchFriends,
|
||||
exportFriends,
|
||||
fetchFollowers,
|
||||
|
@ -1518,6 +1601,7 @@ const apiService = {
|
|||
bookmarkStatus,
|
||||
unbookmarkStatus,
|
||||
postStatus,
|
||||
editStatus,
|
||||
deleteStatus,
|
||||
uploadMedia,
|
||||
setMediaDescription,
|
||||
|
|
|
@ -242,6 +242,16 @@ export const parseAttachment = (data) => {
|
|||
return output
|
||||
}
|
||||
|
||||
export const parseSource = (data) => {
|
||||
const output = {}
|
||||
|
||||
output.text = data.text
|
||||
output.spoiler_text = data.spoiler_text
|
||||
output.content_type = data.content_type
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
export const parseStatus = (data) => {
|
||||
const output = {}
|
||||
const masto = data.hasOwnProperty('account')
|
||||
|
@ -263,6 +273,8 @@ export const parseStatus = (data) => {
|
|||
|
||||
output.tags = data.tags
|
||||
|
||||
output.is_edited = data.edited_at !== null
|
||||
|
||||
if (data.pleroma) {
|
||||
const { pleroma } = data
|
||||
output.text = pleroma.content ? data.pleroma.content['text/plain'] : data.content
|
||||
|
|
|
@ -49,6 +49,47 @@ const postStatus = ({
|
|||
})
|
||||
}
|
||||
|
||||
const editStatus = ({
|
||||
store,
|
||||
statusId,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
media = [],
|
||||
contentType = 'text/plain'
|
||||
}) => {
|
||||
const mediaIds = map(media, 'id')
|
||||
|
||||
return apiService.editStatus({
|
||||
id: statusId,
|
||||
credentials: store.state.users.currentUser.credentials,
|
||||
status,
|
||||
spoilerText,
|
||||
sensitive,
|
||||
poll,
|
||||
mediaIds,
|
||||
contentType
|
||||
})
|
||||
.then((data) => {
|
||||
if (!data.error) {
|
||||
store.dispatch('addNewStatuses', {
|
||||
statuses: [data],
|
||||
timeline: 'friends',
|
||||
showImmediately: true,
|
||||
noIdUpdate: true // To prevent missing notices on next pull.
|
||||
})
|
||||
}
|
||||
return data
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('Error editing status', err)
|
||||
return {
|
||||
error: err.message
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const uploadMedia = ({ store, formData }) => {
|
||||
const credentials = store.state.users.currentUser.credentials
|
||||
return apiService.uploadMedia({ credentials, formData })
|
||||
|
@ -61,6 +102,7 @@ const setMediaDescription = ({ store, id, description }) => {
|
|||
|
||||
const statusPosterService = {
|
||||
postStatus,
|
||||
editStatus,
|
||||
uploadMedia,
|
||||
setMediaDescription
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue