kopia lustrzana https://github.com/pixelfed/pixelfed
				
				
				
			Add Announcements/Newsroom feature
							rodzic
							
								
									279c57d9a5
								
							
						
					
					
						commit
						30c1af7c78
					
				| 
						 | 
				
			
			@ -0,0 +1,92 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App\Http\Controllers;
 | 
			
		||||
 | 
			
		||||
use Auth;
 | 
			
		||||
use App\Newsroom;
 | 
			
		||||
use Illuminate\Support\Str;
 | 
			
		||||
use Illuminate\Http\Request;
 | 
			
		||||
use Illuminate\Support\Facades\Redis;
 | 
			
		||||
 | 
			
		||||
class NewsroomController extends Controller
 | 
			
		||||
{
 | 
			
		||||
 | 
			
		||||
	public function index(Request $request)
 | 
			
		||||
	{
 | 
			
		||||
		if(Auth::check()) {
 | 
			
		||||
			$posts = Newsroom::whereNotNull('published_at')->latest()->paginate(9);
 | 
			
		||||
		} else {
 | 
			
		||||
			$posts = Newsroom::whereNotNull('published_at')
 | 
			
		||||
				->whereAuthOnly(false)
 | 
			
		||||
				->latest()
 | 
			
		||||
				->paginate(3);
 | 
			
		||||
		}
 | 
			
		||||
		return view('site.news.home', compact('posts'));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function show(Request $request, $year, $month, $slug)
 | 
			
		||||
	{
 | 
			
		||||
		$post = Newsroom::whereNotNull('published_at')
 | 
			
		||||
			->whereSlug($slug)
 | 
			
		||||
			->whereYear('published_at', $year)
 | 
			
		||||
			->whereMonth('published_at', $month)
 | 
			
		||||
			->firstOrFail();
 | 
			
		||||
		abort_if($post->auth_only && !$request->user(), 404);
 | 
			
		||||
		return view('site.news.post.show', compact('post'));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function search(Request $request)
 | 
			
		||||
	{
 | 
			
		||||
		$this->validate($request, [
 | 
			
		||||
			'q'			=> 'nullable'
 | 
			
		||||
		]);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function archive(Request $request)
 | 
			
		||||
	{
 | 
			
		||||
		return view('site.news.archive.index');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function timelineApi(Request $request)
 | 
			
		||||
	{
 | 
			
		||||
		abort_if(!Auth::check(), 404);
 | 
			
		||||
 | 
			
		||||
		$key = 'newsroom:read:profileid:' . $request->user()->profile_id;
 | 
			
		||||
		$read = Redis::smembers($key);
 | 
			
		||||
 | 
			
		||||
		$posts = Newsroom::whereNotNull('published_at')
 | 
			
		||||
			->whereShowTimeline(true)
 | 
			
		||||
			->whereNotIn('id', $read)
 | 
			
		||||
			->orderBy('id', 'desc')
 | 
			
		||||
			->take(9)
 | 
			
		||||
			->get()
 | 
			
		||||
			->map(function($post) {
 | 
			
		||||
				return [
 | 
			
		||||
					'id' => $post->id,
 | 
			
		||||
					'title' => Str::limit($post->title, 25),
 | 
			
		||||
					'summary' => $post->summary,
 | 
			
		||||
					'url' => $post->show_link ? $post->permalink() : null,
 | 
			
		||||
					'published_at' => $post->published_at->format('F m, Y')
 | 
			
		||||
				];
 | 
			
		||||
			});
 | 
			
		||||
		return response()->json($posts, 200, [], JSON_PRETTY_PRINT);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	public function markAsRead(Request $request)
 | 
			
		||||
	{
 | 
			
		||||
		abort_if(!Auth::check(), 404);
 | 
			
		||||
 | 
			
		||||
		$this->validate($request, [
 | 
			
		||||
			'id' => 'required|integer|min:1'
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		$news = Newsroom::whereNotNull('published_at')
 | 
			
		||||
			->findOrFail($request->input('id'));
 | 
			
		||||
 | 
			
		||||
		$key = 'newsroom:read:profileid:' . $request->user()->profile_id;
 | 
			
		||||
 | 
			
		||||
		Redis::sadd($key, $news->id);
 | 
			
		||||
 | 
			
		||||
		return response()->json(['code' => 200]);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,22 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
namespace App;
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Eloquent\Model;
 | 
			
		||||
 | 
			
		||||
class Newsroom extends Model
 | 
			
		||||
{
 | 
			
		||||
    protected $table = 'newsroom';
 | 
			
		||||
    protected $fillable = ['title'];
 | 
			
		||||
 | 
			
		||||
    protected $dates = ['published_at'];
 | 
			
		||||
 | 
			
		||||
    public function permalink()
 | 
			
		||||
    {
 | 
			
		||||
    	$year = $this->published_at->year;
 | 
			
		||||
    	$month = $this->published_at->format('m');
 | 
			
		||||
    	$slug = $this->slug;
 | 
			
		||||
 | 
			
		||||
    	return url("/site/newsroom/{$year}/{$month}/{$slug}");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,45 @@
 | 
			
		|||
<?php
 | 
			
		||||
 | 
			
		||||
use Illuminate\Database\Migrations\Migration;
 | 
			
		||||
use Illuminate\Database\Schema\Blueprint;
 | 
			
		||||
use Illuminate\Support\Facades\Schema;
 | 
			
		||||
 | 
			
		||||
class CreateNewsroomTable extends Migration
 | 
			
		||||
{
 | 
			
		||||
    /**
 | 
			
		||||
     * Run the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function up()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::create('newsroom', function (Blueprint $table) {
 | 
			
		||||
            $table->bigIncrements('id');
 | 
			
		||||
            $table->bigInteger('user_id')->unsigned()->nullable();
 | 
			
		||||
            $table->string('header_photo_url')->nullable();
 | 
			
		||||
            $table->string('title')->nullable();
 | 
			
		||||
            $table->string('slug')->nullable()->unique()->index();
 | 
			
		||||
            $table->string('category')->default('update');
 | 
			
		||||
            $table->text('summary')->nullable();
 | 
			
		||||
            $table->text('body')->nullable();
 | 
			
		||||
            $table->text('body_rendered')->nullable();
 | 
			
		||||
            $table->string('link')->nullable();
 | 
			
		||||
            $table->boolean('force_modal')->default(false);
 | 
			
		||||
            $table->boolean('show_timeline')->default(false);
 | 
			
		||||
            $table->boolean('show_link')->default(false);
 | 
			
		||||
            $table->boolean('auth_only')->default(true);
 | 
			
		||||
            $table->timestamp('published_at')->nullable();
 | 
			
		||||
            $table->timestamps();
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Reverse the migrations.
 | 
			
		||||
     *
 | 
			
		||||
     * @return void
 | 
			
		||||
     */
 | 
			
		||||
    public function down()
 | 
			
		||||
    {
 | 
			
		||||
        Schema::dropIfExists('site_news');
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,155 @@
 | 
			
		|||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<transition name="fade">
 | 
			
		||||
		<div v-if="announcements.length" class="card border shadow-none mb-3" style="max-width: 18rem;">
 | 
			
		||||
			<div class="card-body">
 | 
			
		||||
				<div class="card-title mb-0">
 | 
			
		||||
					<span class="font-weight-bold">{{announcement.title}}</span>
 | 
			
		||||
					<span class="float-right cursor-pointer" title="Close" @click="close"><i class="fas fa-times text-lighter"></i></span>
 | 
			
		||||
				</div>
 | 
			
		||||
				<p class="card-text">
 | 
			
		||||
					<span style="font-size:13px;">{{announcement.summary}}</span>
 | 
			
		||||
				</p>
 | 
			
		||||
				<p class="d-flex align-items-center justify-content-between mb-0">
 | 
			
		||||
					<a v-if="announcement.url" :href="announcement.url" class="small font-weight-bold mb-0">Read more</a>
 | 
			
		||||
					<span v-else></span>
 | 
			
		||||
					<span>
 | 
			
		||||
						<span :class="[showPrev ? 'btn btn-outline-secondary btn-sm py-0':'btn btn-outline-secondary btn-sm py-0 disabled']" :disabled="showPrev == false" @click="loadPrev()">
 | 
			
		||||
							<i class="fas fa-chevron-left fa-sm"></i>
 | 
			
		||||
						</span>
 | 
			
		||||
						<span class="btn btn-outline-success btn-sm py-0 mx-1" title="Mark as Read" data-toggle="tooltip" data-placement="bottom" @click="markAsRead()">
 | 
			
		||||
							<i class="fas fa-check fa-sm"></i>
 | 
			
		||||
						</span>
 | 
			
		||||
						<span :class="[showNext ? 'btn btn-outline-secondary btn-sm py-0':'btn btn-outline-secondary btn-sm py-0 disabled']" :disabled="showNext == false" @click="loadNext()">
 | 
			
		||||
							<i class="fas fa-chevron-right fa-sm"></i>
 | 
			
		||||
						</span>
 | 
			
		||||
					</span>
 | 
			
		||||
				</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
	</transition>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style type="text/css" scoped>
 | 
			
		||||
.fade-enter-active, .fade-leave-active {
 | 
			
		||||
  transition: opacity .5s;
 | 
			
		||||
}
 | 
			
		||||
.fade-enter, .fade-leave-to {
 | 
			
		||||
  opacity: 0;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 | 
			
		||||
<script type="text/javascript">
 | 
			
		||||
export default {
 | 
			
		||||
	data() {
 | 
			
		||||
		return {
 | 
			
		||||
			announcements: [],
 | 
			
		||||
			announcement: {},
 | 
			
		||||
			cursor: 0,
 | 
			
		||||
			showNext: true,
 | 
			
		||||
			showPrev: false
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	mounted() {
 | 
			
		||||
		this.fetchAnnouncements();
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	updated() {
 | 
			
		||||
		$('[data-toggle="tooltip"]').tooltip()
 | 
			
		||||
	},
 | 
			
		||||
 | 
			
		||||
	methods: {
 | 
			
		||||
		fetchAnnouncements() {
 | 
			
		||||
			let self = this;
 | 
			
		||||
			let key = 'metro-tips-closed';
 | 
			
		||||
			let cached = JSON.parse(window.localStorage.getItem(key));
 | 
			
		||||
			axios.get('/api/v1/pixelfed/newsroom/timeline')
 | 
			
		||||
			.then(res => {
 | 
			
		||||
				self.announcements = res.data.filter(p => {
 | 
			
		||||
					if(cached) {
 | 
			
		||||
						return cached.indexOf(p.id) == -1;
 | 
			
		||||
					} else {
 | 
			
		||||
						return true;
 | 
			
		||||
					}
 | 
			
		||||
				});
 | 
			
		||||
				self.announcement = self.announcements[0]
 | 
			
		||||
				if(self.announcements.length == 1) {
 | 
			
		||||
					self.showNext = false;
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		loadNext() {
 | 
			
		||||
			if(!this.showNext) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			this.cursor += 1;
 | 
			
		||||
			this.announcement = this.announcements[this.cursor];
 | 
			
		||||
			if((this.cursor + 1) == this.announcements.length) {
 | 
			
		||||
				this.showNext = false;
 | 
			
		||||
			}
 | 
			
		||||
			if(this.cursor >= 1) {
 | 
			
		||||
				this.showPrev = true;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		loadPrev() {
 | 
			
		||||
			if(!this.showPrev) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			this.cursor -= 1;
 | 
			
		||||
			this.announcement = this.announcements[this.cursor];
 | 
			
		||||
			if(this.cursor == 0) {
 | 
			
		||||
				this.showPrev = false;
 | 
			
		||||
			}
 | 
			
		||||
			if(this.cursor < this.announcements.length) {
 | 
			
		||||
				this.showNext = true;
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		closeNewsroomPost(id, index) {
 | 
			
		||||
			let key = 'metro-tips-closed';
 | 
			
		||||
			let ctx = [];
 | 
			
		||||
			let cached = window.localStorage.getItem(key);
 | 
			
		||||
			if(cached) {
 | 
			
		||||
				ctx = JSON.parse(cached);
 | 
			
		||||
			}
 | 
			
		||||
			ctx.push(id);
 | 
			
		||||
			window.localStorage.setItem(key, JSON.stringify(ctx));
 | 
			
		||||
			this.newsroomPosts = this.newsroomPosts.filter(res => {
 | 
			
		||||
				return res.id !== id
 | 
			
		||||
			});
 | 
			
		||||
			if(this.newsroomPosts.length == 0) {
 | 
			
		||||
				this.showTips = false;
 | 
			
		||||
			} else {
 | 
			
		||||
				this.newsroomPost = [ this.newsroomPosts[0] ];
 | 
			
		||||
			}
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		close() {
 | 
			
		||||
			window.localStorage.setItem('metro-tips', false);
 | 
			
		||||
			this.$emit('show-tips', false);
 | 
			
		||||
		},
 | 
			
		||||
 | 
			
		||||
		markAsRead() {
 | 
			
		||||
			let vm = this;
 | 
			
		||||
			axios.post('/api/pixelfed/v1/newsroom/markasread', {
 | 
			
		||||
				id: this.announcement.id
 | 
			
		||||
			})
 | 
			
		||||
			.then(res => {
 | 
			
		||||
				let cur = vm.cursor;
 | 
			
		||||
				vm.announcements.splice(cur, 1);
 | 
			
		||||
				vm.announcement = vm.announcements[0];
 | 
			
		||||
				vm.cursor = 0;
 | 
			
		||||
				vm.showPrev = false;
 | 
			
		||||
				vm.showNext = vm.announcements.length > 1;
 | 
			
		||||
			})
 | 
			
		||||
			.catch(err => {
 | 
			
		||||
				swal('Oops, Something went wrong', 'There was a problem with your request, please try again later.', 'error');
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
</script>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
@extends('site.news.partial.layout')
 | 
			
		||||
 | 
			
		||||
@section('body')
 | 
			
		||||
<div class="container">
 | 
			
		||||
	<p class="text-center">Archive here</p>
 | 
			
		||||
</div>
 | 
			
		||||
@endsection
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,26 @@
 | 
			
		|||
@extends('site.news.partial.layout')
 | 
			
		||||
 | 
			
		||||
@section('body')
 | 
			
		||||
<div class="container">
 | 
			
		||||
	<div class="row px-3">
 | 
			
		||||
		@foreach($posts->slice(0,1) as $post)
 | 
			
		||||
		<div class="col-12 bg-light d-flex justify-content-center align-items-center mt-2 mb-4" style="height:300px;">
 | 
			
		||||
			<div class="mx-5">
 | 
			
		||||
				<p class="small text-danger mb-0 text-uppercase">{{$post->category}}</p>
 | 
			
		||||
				<p class="small text-muted">{{$post->published_at->format('F d, Y')}}</p>
 | 
			
		||||
				<p class="h1" style="font-size: 2.6rem;font-weight: 700;"><a class="text-dark text-decoration-none" href="{{$post->permalink()}}">{{$post->title}}</a></p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		@endforeach
 | 
			
		||||
		@foreach($posts->slice(1) as $post)
 | 
			
		||||
		<div class="col-6 bg-light d-flex justify-content-center align-items-center mt-3 px-5" style="height:300px;">
 | 
			
		||||
			<div class="mx-0">
 | 
			
		||||
				<p class="small text-danger mb-0 text-uppercase">{{$post->category}}</p>
 | 
			
		||||
				<p class="small text-muted">{{$post->published_at->format('F d, Y')}}</p>
 | 
			
		||||
				<p class="h1" style="font-size: 2rem;font-weight: 700;"><a class="text-dark text-decoration-none" href="{{$post->permalink()}}">{{$post->title}}</a></p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		@endforeach
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@endsection
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,17 @@
 | 
			
		|||
@extends('layouts.anon')
 | 
			
		||||
 | 
			
		||||
@section('content')
 | 
			
		||||
 @include('site.news.partial.nav')
 | 
			
		||||
 @yield('body');
 | 
			
		||||
@endsection
 | 
			
		||||
 | 
			
		||||
@push('styles')
 | 
			
		||||
<style type="text/css">
 | 
			
		||||
	html, body {
 | 
			
		||||
		background: #fff;
 | 
			
		||||
	}
 | 
			
		||||
	.navbar-laravel {
 | 
			
		||||
		box-shadow: none;
 | 
			
		||||
	}
 | 
			
		||||
</style>
 | 
			
		||||
@endpush
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
<div class="container py-4">
 | 
			
		||||
	<div class="col-12 d-flex justify-content-between border-bottom pb-1 px-0">
 | 
			
		||||
		<div>
 | 
			
		||||
			<p class="h4"><a href="/site/newsroom" class="text-dark text-decoration-none">Newsroom</a></p>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div>
 | 
			
		||||
			<a href="/site/newsroom/search" class="small text-muted mr-4 text-decoration-none">Search Newsroom</a>
 | 
			
		||||
			<a href="/site/newsroom/archive" class="small text-muted text-decoration-none">Archive</a>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
@extends('site.news.partial.layout')
 | 
			
		||||
 | 
			
		||||
@section('body')
 | 
			
		||||
<div class="container mt-3">
 | 
			
		||||
	<div class="row px-3">
 | 
			
		||||
		<div class="col-12 bg-light d-flex justify-content-center align-items-center" style="min-height: 400px">
 | 
			
		||||
			<div style="max-width: 550px;">
 | 
			
		||||
				<p class="small text-danger mb-0 text-uppercase">{{$post->category}}</p>
 | 
			
		||||
				<p class="small text-muted">{{$post->published_at->format('F d, Y')}}</p>
 | 
			
		||||
				<p class="h1" style="font-size: 2.6rem;font-weight: 700;">{{$post->title}}</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div class="col-12 mt-4">
 | 
			
		||||
			<div class="d-flex justify-content-center">
 | 
			
		||||
				<p class="lead text-center py-5" style="font-size:25px; font-weight: 200; max-width: 550px;">
 | 
			
		||||
					{{$post->summary}}
 | 
			
		||||
				</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		@if($post->body)
 | 
			
		||||
		<div class="col-12 mt-4">
 | 
			
		||||
			<div class="d-flex justify-content-center border-top">
 | 
			
		||||
				<p class="lead py-5" style="max-width: 550px;">
 | 
			
		||||
					{!!$post->body!!}
 | 
			
		||||
				</p>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		@else
 | 
			
		||||
		<div class="col-12 mt-4"></div>
 | 
			
		||||
		@endif
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@endsection
 | 
			
		||||
		Ładowanie…
	
		Reference in New Issue