diff --git a/app/Http/Controllers/StatusEditController.php b/app/Http/Controllers/StatusEditController.php
new file mode 100644
index 000000000..1d0a22396
--- /dev/null
+++ b/app/Http/Controllers/StatusEditController.php
@@ -0,0 +1,59 @@
+middleware('auth');
+ abort_if(!config('exp.pue'), 404, 'Post editing is not enabled on this server.');
+ }
+
+ public function store(StoreStatusEditRequest $request, $id)
+ {
+ $validated = $request->validated();
+
+ $status = Status::findOrFail($id);
+ abort_if(StatusEdit::whereStatusId($status->id)->count() >= 10, 400, 'You cannot edit your post more than 10 times.');
+ $res = UpdateStatusService::call($status, $validated);
+
+ $status = Status::findOrFail($id);
+ StatusLocalUpdateActivityPubDeliverPipeline::dispatch($status)->delay(now()->addMinutes(1));
+ return $res;
+ }
+
+ public function history(Request $request, $id)
+ {
+ abort_if(!$request->user(), 403);
+ $status = Status::whereNull('reblog_of_id')->findOrFail($id);
+ abort_if(!in_array($status->scope, ['public', 'unlisted']), 403);
+ if(!$status->edits()->count()) {
+ return [];
+ }
+ $cached = StatusService::get($status->id, false);
+
+ $res = $status->edits->map(function($edit) use($cached) {
+ return [
+ 'content' => Autolink::create()->autolink($edit->caption),
+ 'spoiler_text' => $edit->spoiler_text,
+ 'sensitive' => (bool) $edit->is_nsfw,
+ 'created_at' => str_replace('+00:00', 'Z', $edit->created_at->format(DATE_RFC3339_EXTENDED)),
+ 'account' => $cached['account'],
+ 'media_attachments' => $cached['media_attachments'],
+ 'emojis' => $cached['emojis'],
+ ];
+ })->reverse()->values()->toArray();
+ return $res;
+ }
+}
diff --git a/app/Http/Requests/Status/StoreStatusEditRequest.php b/app/Http/Requests/Status/StoreStatusEditRequest.php
new file mode 100644
index 000000000..aa9364ca6
--- /dev/null
+++ b/app/Http/Requests/Status/StoreStatusEditRequest.php
@@ -0,0 +1,69 @@
+user()->profile;
+ if($profile->status != null) {
+ return false;
+ }
+ if($profile->unlisted == true && $profile->cw == true) {
+ return false;
+ }
+ $types = [
+ "photo",
+ "photo:album",
+ "photo:video:album",
+ "reply",
+ "text",
+ "video",
+ "video:album"
+ ];
+ $scopes = ['public', 'unlisted', 'private'];
+ $status = Status::whereNull('reblog_of_id')->whereIn('type', $types)->whereIn('scope', $scopes)->find($this->route('id'));
+ return $status && $this->user()->profile_id === $status->profile_id;
+ }
+
+ /**
+ * Get the validation rules that apply to the request.
+ *
+ * @return array
+ */
+ public function rules(): array
+ {
+ return [
+ 'status' => 'sometimes|max:'.config('pixelfed.max_caption_length', 500),
+ 'spoiler_text' => 'nullable|string|max:140',
+ 'sensitive' => 'sometimes|boolean',
+ 'media_ids' => [
+ 'nullable',
+ 'required_without:status',
+ 'array',
+ 'max:' . config('pixelfed.max_album_length'),
+ function (string $attribute, mixed $value, Closure $fail) {
+ Media::whereProfileId($this->user()->profile_id)
+ ->where(function($query) {
+ return $query->whereNull('status_id')
+ ->orWhere('status_id', '=', $this->route('id'));
+ })
+ ->findOrFail($value);
+ },
+ ],
+ 'location' => 'sometimes|nullable',
+ 'location.id' => 'sometimes|integer|min:1|max:128769',
+ 'location.country' => 'required_with:location.id',
+ 'location.name' => 'required_with:location.id',
+ ];
+ }
+}
diff --git a/app/Jobs/StatusPipeline/StatusLocalUpdateActivityPubDeliverPipeline.php b/app/Jobs/StatusPipeline/StatusLocalUpdateActivityPubDeliverPipeline.php
new file mode 100644
index 000000000..745f5f5ff
--- /dev/null
+++ b/app/Jobs/StatusPipeline/StatusLocalUpdateActivityPubDeliverPipeline.php
@@ -0,0 +1,129 @@
+status = $status;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ $status = $this->status;
+ $profile = $status->profile;
+
+ // ignore group posts
+ // if($status->group_id != null) {
+ // return;
+ // }
+
+ if($status->local == false || $status->url || $status->uri) {
+ return;
+ }
+
+ $audience = $status->profile->getAudienceInbox();
+
+ if(empty($audience) || !in_array($status->scope, ['public', 'unlisted', 'private'])) {
+ // Return on profiles with no remote followers
+ return;
+ }
+
+ switch($status->type) {
+ case 'poll':
+ // Polls not yet supported
+ return;
+ break;
+
+ default:
+ $activitypubObject = new UpdateNote();
+ break;
+ }
+
+
+ $fractal = new Fractal\Manager();
+ $fractal->setSerializer(new ArraySerializer());
+ $resource = new Fractal\Resource\Item($status, $activitypubObject);
+ $activity = $fractal->createData($resource)->toArray();
+
+ $payload = json_encode($activity);
+
+ $client = new Client([
+ 'timeout' => config('federation.activitypub.delivery.timeout')
+ ]);
+
+ $version = config('pixelfed.version');
+ $appUrl = config('app.url');
+ $userAgent = "(Pixelfed/{$version}; +{$appUrl})";
+
+ $requests = function($audience) use ($client, $activity, $profile, $payload, $userAgent) {
+ foreach($audience as $url) {
+ $headers = HttpSignature::sign($profile, $url, $activity, [
+ 'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
+ 'User-Agent' => $userAgent,
+ ]);
+ yield function() use ($client, $url, $headers, $payload) {
+ return $client->postAsync($url, [
+ 'curl' => [
+ CURLOPT_HTTPHEADER => $headers,
+ CURLOPT_POSTFIELDS => $payload,
+ CURLOPT_HEADER => true,
+ CURLOPT_SSL_VERIFYPEER => false,
+ CURLOPT_SSL_VERIFYHOST => false
+ ]
+ ]);
+ };
+ }
+ };
+
+ $pool = new Pool($client, $requests($audience), [
+ 'concurrency' => config('federation.activitypub.delivery.concurrency'),
+ 'fulfilled' => function ($response, $index) {
+ },
+ 'rejected' => function ($reason, $index) {
+ }
+ ]);
+
+ $promise = $pool->promise();
+
+ $promise->wait();
+ }
+}
diff --git a/app/Models/StatusEdit.php b/app/Models/StatusEdit.php
new file mode 100644
index 000000000..5c9ec5695
--- /dev/null
+++ b/app/Models/StatusEdit.php
@@ -0,0 +1,19 @@
+ 'array',
+ 'media_descriptions' => 'array',
+ 'poll_options' => 'array'
+ ];
+
+ protected $guarded = [];
+}
diff --git a/app/Services/Status/UpdateStatusService.php b/app/Services/Status/UpdateStatusService.php
new file mode 100644
index 000000000..be50bf70f
--- /dev/null
+++ b/app/Services/Status/UpdateStatusService.php
@@ -0,0 +1,137 @@
+id);
+ }
+
+ public static function updateMediaAttachements(Status $status, $attributes)
+ {
+ $count = $status->media()->count();
+ if($count === 0 || $count === 1) {
+ return;
+ }
+
+ $oids = $status->media()->orderBy('order')->pluck('id')->map(function($m) { return (string) $m; });
+ $nids = collect($attributes['media_ids']);
+
+ if($oids->toArray() === $nids->toArray()) {
+ return;
+ }
+
+ foreach($oids->diff($nids)->values()->toArray() as $mid) {
+ $media = Media::find($mid);
+ if(!$media) {
+ continue;
+ }
+ $media->status_id = null;
+ $media->save();
+ MediaStorageService::delete($media, true);
+ }
+
+ $nids->each(function($nid, $idx) {
+ $media = Media::find($nid);
+ if(!$media) {
+ return;
+ }
+ $media->order = $idx;
+ $media->save();
+ });
+ MediaService::del($status->id);
+ }
+
+ public static function handleImmediateAttributes(Status $status, $attributes)
+ {
+ if(isset($attributes['status'])) {
+ $cleaned = Purify::clean($attributes['status']);
+ $status->caption = $cleaned;
+ $status->rendered = Autolink::create()->autolink($cleaned);
+ } else {
+ $status->caption = null;
+ $status->rendered = null;
+ }
+ if(isset($attributes['sensitive'])) {
+ if($status->is_nsfw != (bool) $attributes['sensitive'] &&
+ (bool) $attributes['sensitive'] == false)
+ {
+ $exists = ModLog::whereObjectType('App\Status::class')
+ ->whereObjectId($status->id)
+ ->whereAction('admin.status.moderate')
+ ->exists();
+ if(!$exists) {
+ $status->is_nsfw = (bool) $attributes['sensitive'];
+ }
+ } else {
+ $status->is_nsfw = (bool) $attributes['sensitive'];
+ }
+ }
+ if(isset($attributes['spoiler_text'])) {
+ $status->cw_summary = Purify::clean($attributes['spoiler_text']);
+ } else {
+ $status->cw_summary = null;
+ }
+ if(isset($attributes['location'])) {
+ if (isset($attributes['location']['id'])) {
+ $status->place_id = $attributes['location']['id'];
+ } else {
+ $status->place_id = null;
+ }
+ }
+ if($status->cw_summary && !$status->is_nsfw) {
+ $status->cw_summary = null;
+ }
+ $status->edited_at = now();
+ $status->save();
+ StatusService::del($status->id);
+ }
+
+ public static function createPreviousEdit(Status $status)
+ {
+ if(!$status->edits()->count()) {
+ StatusEdit::create([
+ 'status_id' => $status->id,
+ 'profile_id' => $status->profile_id,
+ 'caption' => $status->caption,
+ 'spoiler_text' => $status->cw_summary,
+ 'is_nsfw' => $status->is_nsfw,
+ 'ordered_media_attachment_ids' => $status->media()->orderBy('order')->pluck('id')->toArray(),
+ 'created_at' => $status->created_at
+ ]);
+ }
+ }
+
+ public static function createEdit(Status $status, $attributes)
+ {
+ $cleaned = isset($attributes['status']) ? Purify::clean($attributes['status']) : null;
+ $spoiler_text = isset($attributes['spoiler_text']) ? Purify::clean($attributes['spoiler_text']) : null;
+ $sensitive = isset($attributes['sensitive']) ? $attributes['sensitive'] : null;
+ $mids = $status->media()->count() ? $status->media()->orderBy('order')->pluck('id')->toArray() : null;
+ StatusEdit::create([
+ 'status_id' => $status->id,
+ 'profile_id' => $status->profile_id,
+ 'caption' => $cleaned,
+ 'spoiler_text' => $spoiler_text,
+ 'is_nsfw' => $sensitive,
+ 'ordered_media_attachment_ids' => $mids
+ ]);
+ }
+}
diff --git a/app/Status.php b/app/Status.php
index 183c18023..4148a4f99 100644
--- a/app/Status.php
+++ b/app/Status.php
@@ -9,6 +9,7 @@ use App\Http\Controllers\StatusController;
use Illuminate\Database\Eloquent\SoftDeletes;
use App\Models\Poll;
use App\Services\AccountService;
+use App\Models\StatusEdit;
class Status extends Model
{
@@ -27,7 +28,8 @@ class Status extends Model
* @var array
*/
protected $casts = [
- 'deleted_at' => 'datetime'
+ 'deleted_at' => 'datetime',
+ 'edited_at' => 'datetime'
];
protected $guarded = [];
@@ -393,4 +395,9 @@ class Status extends Model
{
return $this->hasOne(Poll::class);
}
+
+ public function edits()
+ {
+ return $this->hasMany(StatusEdit::class);
+ }
}
diff --git a/app/Transformer/ActivityPub/Verb/UpdateNote.php b/app/Transformer/ActivityPub/Verb/UpdateNote.php
new file mode 100644
index 000000000..bdbb20c45
--- /dev/null
+++ b/app/Transformer/ActivityPub/Verb/UpdateNote.php
@@ -0,0 +1,133 @@
+mentions->map(function ($mention) {
+ $webfinger = $mention->emailUrl();
+ $name = Str::startsWith($webfinger, '@') ?
+ $webfinger :
+ '@' . $webfinger;
+ return [
+ 'type' => 'Mention',
+ 'href' => $mention->permalink(),
+ 'name' => $name
+ ];
+ })->toArray();
+
+ if($status->in_reply_to_id != null) {
+ $parent = $status->parent()->profile;
+ if($parent) {
+ $webfinger = $parent->emailUrl();
+ $name = Str::startsWith($webfinger, '@') ?
+ $webfinger :
+ '@' . $webfinger;
+ $reply = [
+ 'type' => 'Mention',
+ 'href' => $parent->permalink(),
+ 'name' => $name
+ ];
+ $mentions = array_merge($reply, $mentions);
+ }
+ }
+
+ $hashtags = $status->hashtags->map(function ($hashtag) {
+ return [
+ 'type' => 'Hashtag',
+ 'href' => $hashtag->url(),
+ 'name' => "#{$hashtag->name}",
+ ];
+ })->toArray();
+
+ $emojis = CustomEmoji::scan($status->caption, true) ?? [];
+ $emoji = array_merge($emojis, $mentions);
+ $tags = array_merge($emoji, $hashtags);
+
+ $latestEdit = $status->edits()->latest()->first();
+
+ return [
+ '@context' => [
+ 'https://w3id.org/security/v1',
+ 'https://www.w3.org/ns/activitystreams',
+ [
+ 'Hashtag' => 'as:Hashtag',
+ 'sensitive' => 'as:sensitive',
+ 'schema' => 'http://schema.org/',
+ 'pixelfed' => 'http://pixelfed.org/ns#',
+ 'commentsEnabled' => [
+ '@id' => 'pixelfed:commentsEnabled',
+ '@type' => 'schema:Boolean'
+ ],
+ 'capabilities' => [
+ '@id' => 'pixelfed:capabilities',
+ '@container' => '@set'
+ ],
+ 'announce' => [
+ '@id' => 'pixelfed:canAnnounce',
+ '@type' => '@id'
+ ],
+ 'like' => [
+ '@id' => 'pixelfed:canLike',
+ '@type' => '@id'
+ ],
+ 'reply' => [
+ '@id' => 'pixelfed:canReply',
+ '@type' => '@id'
+ ],
+ 'toot' => 'http://joinmastodon.org/ns#',
+ 'Emoji' => 'toot:Emoji'
+ ]
+ ],
+ 'id' => $status->permalink('#updates/' . $latestEdit->id),
+ 'type' => 'Update',
+ 'actor' => $status->profile->permalink(),
+ 'published' => $latestEdit->created_at->toAtomString(),
+ 'to' => $status->scopeToAudience('to'),
+ 'cc' => $status->scopeToAudience('cc'),
+ 'object' => [
+ 'id' => $status->url(),
+ 'type' => 'Note',
+ 'summary' => $status->is_nsfw ? $status->cw_summary : null,
+ 'content' => $status->rendered ?? $status->caption,
+ 'inReplyTo' => $status->in_reply_to_id ? $status->parent()->url() : null,
+ 'published' => $status->created_at->toAtomString(),
+ 'url' => $status->url(),
+ 'attributedTo' => $status->profile->permalink(),
+ 'to' => $status->scopeToAudience('to'),
+ 'cc' => $status->scopeToAudience('cc'),
+ 'sensitive' => (bool) $status->is_nsfw,
+ 'attachment' => $status->media()->orderBy('order')->get()->map(function ($media) {
+ return [
+ 'type' => $media->activityVerb(),
+ 'mediaType' => $media->mime,
+ 'url' => $media->url(),
+ 'name' => $media->caption,
+ ];
+ })->toArray(),
+ 'tag' => $tags,
+ 'commentsEnabled' => (bool) !$status->comments_disabled,
+ 'updated' => $latestEdit->created_at->toAtomString(),
+ 'capabilities' => [
+ 'announce' => 'https://www.w3.org/ns/activitystreams#Public',
+ 'like' => 'https://www.w3.org/ns/activitystreams#Public',
+ 'reply' => $status->comments_disabled == true ? '[]' : 'https://www.w3.org/ns/activitystreams#Public'
+ ],
+ 'location' => $status->place_id ? [
+ 'type' => 'Place',
+ 'name' => $status->place->name,
+ 'longitude' => $status->place->long,
+ 'latitude' => $status->place->lat,
+ 'country' => $status->place->country
+ ] : null,
+ ]
+ ];
+ }
+}
diff --git a/app/Transformer/Api/StatusStatelessTransformer.php b/app/Transformer/Api/StatusStatelessTransformer.php
index 38a3fedf8..acb522446 100644
--- a/app/Transformer/Api/StatusStatelessTransformer.php
+++ b/app/Transformer/Api/StatusStatelessTransformer.php
@@ -65,7 +65,8 @@ class StatusStatelessTransformer extends Fractal\TransformerAbstract
'media_attachments' => MediaService::get($status->id),
'account' => AccountService::get($status->profile_id, true),
'tags' => StatusHashtagService::statusTags($status->id),
- 'poll' => $poll
+ 'poll' => $poll,
+ 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
];
}
}
diff --git a/app/Transformer/Api/StatusTransformer.php b/app/Transformer/Api/StatusTransformer.php
index 61c5f875b..f735a57be 100644
--- a/app/Transformer/Api/StatusTransformer.php
+++ b/app/Transformer/Api/StatusTransformer.php
@@ -70,6 +70,7 @@ class StatusTransformer extends Fractal\TransformerAbstract
'tags' => StatusHashtagService::statusTags($status->id),
'poll' => $poll,
'bookmarked' => BookmarkService::get($pid, $status->id),
+ 'edited_at' => $status->edited_at ? str_replace('+00:00', 'Z', $status->edited_at->format(DATE_RFC3339_EXTENDED)) : null,
];
}
}
diff --git a/app/Util/ActivityPub/Inbox.php b/app/Util/ActivityPub/Inbox.php
index 790a48e8d..5ac29f916 100644
--- a/app/Util/ActivityPub/Inbox.php
+++ b/app/Util/ActivityPub/Inbox.php
@@ -28,6 +28,7 @@ use App\Jobs\DeletePipeline\DeleteRemoteProfilePipeline;
use App\Jobs\DeletePipeline\DeleteRemoteStatusPipeline;
use App\Jobs\StoryPipeline\StoryExpire;
use App\Jobs\StoryPipeline\StoryFetch;
+use App\Jobs\StatusPipeline\StatusRemoteUpdatePipeline;
use App\Util\ActivityPub\Validator\Accept as AcceptValidator;
use App\Util\ActivityPub\Validator\Add as AddValidator;
@@ -128,9 +129,9 @@ class Inbox
$this->handleFlagActivity();
break;
- // case 'Update':
- // (new UpdateActivity($this->payload, $this->profile))->handle();
- // break;
+ case 'Update':
+ $this->handleUpdateActivity();
+ break;
default:
// TODO: decide how to handle invalid verbs.
@@ -1207,4 +1208,23 @@ class Inbox
return;
}
+
+ public function handleUpdateActivity()
+ {
+ $activity = $this->payload['object'];
+ $actor = $this->actorFirstOrCreate($this->payload['actor']);
+ if(!$actor || $actor->domain == null) {
+ return;
+ }
+
+ if(!isset($activity['type'], $activity['id'])) {
+ return;
+ }
+
+ if($activity['type'] === 'Note') {
+ if(Status::whereObjectUrl($activity['id'])->exists()) {
+ StatusRemoteUpdatePipeline::dispatch($actor, $activity);
+ }
+ }
+ }
}
diff --git a/config/exp.php b/config/exp.php
index 0c9f83706..589572b5e 100644
--- a/config/exp.php
+++ b/config/exp.php
@@ -25,6 +25,8 @@ return [
// Cached public timeline for larger instances (beta)
'cached_public_timeline' => env('EXP_CPT', false),
+ 'cached_home_timeline' => env('EXP_CHT', false),
+
// Groups (unreleased)
'gps' => env('EXP_GPS', false),
@@ -33,4 +35,10 @@ return [
// Enforce Mastoapi Compatibility (alpha)
'emc' => env('EXP_EMC', true),
+
+ // HLS Live Streaming
+ 'hls' => env('HLS_LIVE', false),
+
+ // Post Update/Edits
+ 'pue' => env('EXP_PUE', false),
];
diff --git a/resources/assets/components/Post.vue b/resources/assets/components/Post.vue
index 842b15dd5..1cc57c84e 100644
--- a/resources/assets/components/Post.vue
+++ b/resources/assets/components/Post.vue
@@ -26,7 +26,7 @@
+
+
@@ -119,6 +125,7 @@
import LikesModal from './partials/post/LikeModal.vue';
import SharesModal from './partials/post/ShareModal.vue';
import ReportModal from './partials/modal/ReportPost.vue';
+ import PostEditModal from './partials/post/PostEditModal.vue';
export default {
props: {
@@ -140,7 +147,8 @@
"likes-modal": LikesModal,
"shares-modal": SharesModal,
"rightbar": Rightbar,
- "report-modal": ReportModal
+ "report-modal": ReportModal,
+ "post-edit-modal": PostEditModal
},
data() {
@@ -156,7 +164,8 @@
isReply: false,
reply: {},
showSharesModal: false,
- postStateError: false
+ postStateError: false,
+ forceUpdateIdx: 0
}
},
@@ -405,6 +414,17 @@
break;
}
},
+
+ handleEdit(status) {
+ this.$refs.editModal.show(status);
+ },
+
+ mergeUpdatedPost(post) {
+ this.post = post;
+ this.$nextTick(() => {
+ this.forceUpdateIdx++;
+ });
+ }
}
}
diff --git a/resources/assets/components/partials/post/EditHistoryModal.vue b/resources/assets/components/partials/post/EditHistoryModal.vue
new file mode 100644
index 000000000..9d0a1366d
--- /dev/null
+++ b/resources/assets/components/partials/post/EditHistoryModal.vue
@@ -0,0 +1,233 @@
+
+
+
+
+
+
+
+
Post History
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ allHistory[0].account.username }}
+
+
+
{{ historyIndex == (allHistory.length - 1) ? 'created' : 'edited' }} {{ formatTime(allHistory[allHistory.length - 1].created_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ history.account.username }}
+
+
{{ idx == (allHistory.length - 1) ? 'created' : 'edited' }} {{ formatTime(history.created_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/components/partials/post/PostHeader.vue b/resources/assets/components/partials/post/PostHeader.vue
new file mode 100644
index 000000000..ddbbf740c
--- /dev/null
+++ b/resources/assets/components/partials/post/PostHeader.vue
@@ -0,0 +1,348 @@
+
+
+
+
+
diff --git a/resources/assets/sass/admin.scss b/resources/assets/sass/admin.scss
index 93c513f82..cb01e4c69 100644
--- a/resources/assets/sass/admin.scss
+++ b/resources/assets/sass/admin.scss
@@ -13,3 +13,23 @@ body, button, input, textarea {
font-size: 30px;
}
}
+
+.nav-pills .nav-item {
+ padding-right: 1rem;
+}
+
+.list-fade-bottom {
+ position: relative;
+
+ &:after {
+ content: "";
+ position: absolute;
+ z-index: 1;
+ bottom: 0;
+ left: 0;
+ pointer-events: none;
+ background-image: linear-gradient(to bottom, rgba(255,255,255, 0), rgba(255,255,255, 1) 90%);
+ width: 100%;
+ height: 10em;
+ }
+}
diff --git a/resources/assets/sass/spa.scss b/resources/assets/sass/spa.scss
index 3fea3e7e2..e2ed0e054 100644
--- a/resources/assets/sass/spa.scss
+++ b/resources/assets/sass/spa.scss
@@ -210,6 +210,7 @@ a.text-dark:hover {
.autocomplete-result-list {
background: var(--light) !important;
+ z-index: 2 !important;
}
.dropdown-menu,
@@ -261,7 +262,8 @@ span.twitter-typeahead .tt-suggestion:focus {
border-color: var(--border-color) !important;
}
-.modal-header {
+.modal-header,
+.modal-footer {
border-color: var(--border-color);
}
@@ -328,3 +330,66 @@ span.twitter-typeahead .tt-suggestion:focus {
}
}
}
+
+.compose-modal-component {
+ .form-control:focus {
+ color: var(--body-color);
+ }
+}
+
+.modal-body {
+ .nav-tabs .nav-link.active,
+ .nav-tabs .nav-item.show .nav-link {
+ background-color: transparent;
+ border-color: var(--border-color);
+ }
+
+ .nav-tabs .nav-link:hover,
+ .nav-tabs .nav-link:focus {
+ border-color: var(--border-color);
+ }
+
+ .form-control:focus {
+ color: var(--body-color);
+ }
+}
+
+.tribute-container {
+ border: 0;
+
+ ul {
+ margin-top: 0;
+ border-color: var(--border-color);
+ }
+
+ li {
+ padding: 0.5rem 1rem;
+ border-top: 0;
+ border-left: 0;
+ border-right: 0;
+ font-size: 13px;
+
+ &:not(:last-child) {
+ border-bottom: 1px solid var(--border-color);
+ }
+
+ &.highlight,
+ &:hover {
+ color: var(--body-color);
+ font-weight: bold;
+ background: rgba(44, 120, 191, 0.25);
+ }
+ }
+}
+
+.timeline-status-component {
+ .username {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
+ margin-bottom: -3px;
+ word-break: break-word;
+
+ @media (min-width: 768px) {
+ font-size: 17px;
+ }
+ }
+}
diff --git a/routes/api.php b/routes/api.php
index 7e996df1d..a0b8b5155 100644
--- a/routes/api.php
+++ b/routes/api.php
@@ -94,6 +94,9 @@ Route::group(['prefix' => 'api'], function() use($middleware) {
Route::post('tags/{id}/follow', 'Api\ApiV1Controller@followHashtag')->middleware($middleware);
Route::post('tags/{id}/unfollow', 'Api\ApiV1Controller@unfollowHashtag')->middleware($middleware);
Route::get('tags/{id}', 'Api\ApiV1Controller@getHashtag')->middleware($middleware);
+
+ Route::get('statuses/{id}/history', 'StatusEditController@history')->middleware($middleware);
+ Route::put('statuses/{id}', 'StatusEditController@store')->middleware($middleware);
});
Route::group(['prefix' => 'v2'], function() use($middleware) {