diff --git a/README.md b/README.md
index 8bd1e80..9567de3 100644
--- a/README.md
+++ b/README.md
@@ -86,6 +86,15 @@ docker-compose run --rm web yarn watch
現在Docker環境でのHMRはサポートしてません。Docker外ならおそらく動くでしょう。
その他詳しくはlaravel-mixのドキュメントなどを当たってください。
+## phpunit によるテスト
+
+変更をしたらPull Requestを投げる前にテストが通ることを確認してください。
+テストは以下のコマンドで実行できます。
+
+```
+docker-compose exec web composer test
+```
+
## 環境構築上の諸注意
- 初版時点では、DB サーバとして PostgreSQL を使うよう .env ファイルを設定するくらいです。
diff --git a/app/Ejaculation.php b/app/Ejaculation.php
index 83b3ad2..24f7e46 100644
--- a/app/Ejaculation.php
+++ b/app/Ejaculation.php
@@ -2,11 +2,15 @@
namespace App;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\DB;
+use Staudenmeir\EloquentEagerLimit\HasEagerLimit;
class Ejaculation extends Model
{
- //
+ use HasEagerLimit;
protected $fillable = [
'user_id', 'ejaculated_date',
@@ -34,4 +38,45 @@ class Ejaculation extends Model
return $v->name;
})->all());
}
+
+ public function likes()
+ {
+ return $this->hasMany(Like::class);
+ }
+
+ public function scopeWithLikes(Builder $query)
+ {
+ if (Auth::check()) {
+ // TODO - このスコープを使うことでlikesが常に直近10件で絞られるのは汚染されすぎ感がある。別名を付与できないか?
+ // - (ejaculation_id, user_id) でユニークなわけですが、is_liked はサブクエリ発行させるのとLeft JoinしてNULLかどうかで結果を見るのどっちがいいんでしょうね
+ return $query
+ ->with([
+ 'likes' => function ($query) {
+ $query->latest()->take(10);
+ },
+ 'likes.user' => function ($query) {
+ $query->where('is_protected', false)
+ ->orWhere('id', Auth::id());
+ }
+ ])
+ ->withCount([
+ 'likes',
+ 'likes as is_liked' => function ($query) {
+ $query->where('user_id', Auth::id());
+ }
+ ]);
+ } else {
+ return $query
+ ->with([
+ 'likes' => function ($query) {
+ $query->latest()->take(10);
+ },
+ 'likes.user' => function ($query) {
+ $query->where('is_protected', false);
+ }
+ ])
+ ->withCount('likes')
+ ->addSelect(DB::raw('0 as is_liked'));
+ }
+ }
}
diff --git a/app/Http/Controllers/Api/LikeController.php b/app/Http/Controllers/Api/LikeController.php
new file mode 100644
index 0000000..d782956
--- /dev/null
+++ b/app/Http/Controllers/Api/LikeController.php
@@ -0,0 +1,73 @@
+validate([
+ 'id' => 'required|integer|exists:ejaculations'
+ ]);
+
+ $keys = [
+ 'user_id' => Auth::id(),
+ 'ejaculation_id' => $request->input('id')
+ ];
+
+ $like = Like::query()->where($keys)->first();
+ if ($like) {
+ $data = [
+ 'errors' => [
+ ['message' => 'このチェックインはすでにいいね済です。']
+ ],
+ 'ejaculation' => $like->ejaculation
+ ];
+
+ return response()->json($data, 409);
+ }
+
+ $like = Like::create($keys);
+
+ return [
+ 'ejaculation' => $like->ejaculation
+ ];
+ }
+
+ public function destroy($id)
+ {
+ Validator::make(compact('id'), [
+ 'id' => 'required|integer'
+ ])->validate();
+
+ $like = Like::query()->where([
+ 'user_id' => Auth::id(),
+ 'ejaculation_id' => $id
+ ])->first();
+ if ($like === null) {
+ $ejaculation = Ejaculation::find($id);
+
+ $data = [
+ 'errors' => [
+ ['message' => 'このチェックインはいいねされていません。']
+ ],
+ 'ejaculation' => $ejaculation
+ ];
+
+ return response()->json($data, 404);
+ }
+
+ $like->delete();
+
+ return [
+ 'ejaculation' => $like->ejaculation
+ ];
+ }
+}
diff --git a/app/Http/Controllers/EjaculationController.php b/app/Http/Controllers/EjaculationController.php
index d2292e0..13ef8c4 100644
--- a/app/Http/Controllers/EjaculationController.php
+++ b/app/Http/Controllers/EjaculationController.php
@@ -78,7 +78,9 @@ class EjaculationController extends Controller
public function show($id)
{
- $ejaculation = Ejaculation::findOrFail($id);
+ $ejaculation = Ejaculation::where('id', $id)
+ ->withLikes()
+ ->firstOrFail();
$user = User::findOrFail($ejaculation->user_id);
// 1つ前のチェックインからの経過時間を求める
diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php
index 95807a5..fb8147f 100644
--- a/app/Http/Controllers/HomeController.php
+++ b/app/Http/Controllers/HomeController.php
@@ -69,6 +69,7 @@ SQL
->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*')
->with('user', 'tags')
+ ->withLikes()
->take(10)
->get();
diff --git a/app/Http/Controllers/SearchController.php b/app/Http/Controllers/SearchController.php
index 271a003..0487344 100644
--- a/app/Http/Controllers/SearchController.php
+++ b/app/Http/Controllers/SearchController.php
@@ -28,6 +28,7 @@ class SearchController extends Controller
->where('is_private', false)
->orderBy('ejaculated_date', 'desc')
->with(['user', 'tags'])
+ ->withLikes()
->paginate(20)
->appends($inputs);
diff --git a/app/Http/Controllers/SettingController.php b/app/Http/Controllers/SettingController.php
index 136f055..1448360 100644
--- a/app/Http/Controllers/SettingController.php
+++ b/app/Http/Controllers/SettingController.php
@@ -46,11 +46,12 @@ class SettingController extends Controller
public function updatePrivacy(Request $request)
{
- $inputs = $request->all(['is_protected', 'accept_analytics']);
+ $inputs = $request->all(['is_protected', 'accept_analytics', 'private_likes']);
$user = Auth::user();
$user->is_protected = $inputs['is_protected'] ?? false;
$user->accept_analytics = $inputs['accept_analytics'] ?? false;
+ $user->private_likes = $inputs['private_likes'] ?? false;
$user->save();
return redirect()->route('setting.privacy')->with('status', 'プライバシー設定を更新しました。');
diff --git a/app/Http/Controllers/TimelineController.php b/app/Http/Controllers/TimelineController.php
index a179537..2f05b71 100644
--- a/app/Http/Controllers/TimelineController.php
+++ b/app/Http/Controllers/TimelineController.php
@@ -16,6 +16,7 @@ class TimelineController extends Controller
->orderBy('ejaculations.ejaculated_date', 'desc')
->select('ejaculations.*')
->with('user', 'tags')
+ ->withLikes()
->paginate(21);
return view('timeline.public')->with(compact('ejaculations'));
diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php
index 1640c78..5bdc9b5 100644
--- a/app/Http/Controllers/UserController.php
+++ b/app/Http/Controllers/UserController.php
@@ -41,6 +41,7 @@ SQL
}
$ejaculations = $query->orderBy('ejaculated_date', 'desc')
->with('tags')
+ ->withLikes()
->paginate(20);
// よく使っているタグ
@@ -176,4 +177,19 @@ SQL
return view('user.profile')->with(compact('user', 'ejaculations'));
}
+
+ public function likes($name)
+ {
+ $user = User::where('name', $name)->first();
+ if (empty($user)) {
+ abort(404);
+ }
+
+ $likes = $user->likes()
+ ->orderBy('created_at', 'desc')
+ ->with('ejaculation.user', 'ejaculation.tags')
+ ->paginate(20);
+
+ return view('user.likes')->with(compact('user', 'likes'));
+ }
}
diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php
index 95714cf..dfa1743 100644
--- a/app/Http/Kernel.php
+++ b/app/Http/Kernel.php
@@ -38,7 +38,12 @@ class Kernel extends HttpKernel
\App\Http\Middleware\NormalizeLineEnding::class,
],
+ // 現時点では内部APIしかないので、認証の手間を省くためにステートフルにしている。
'api' => [
+ \App\Http\Middleware\EncryptCookies::class,
+ \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
+ \Illuminate\Session\Middleware\StartSession::class,
+ \App\Http\Middleware\VerifyCsrfToken::class,
'throttle:60,1',
'bindings',
],
diff --git a/app/Like.php b/app/Like.php
new file mode 100644
index 0000000..b1fa673
--- /dev/null
+++ b/app/Like.php
@@ -0,0 +1,23 @@
+belongsTo(User::class);
+ }
+
+ public function ejaculation()
+ {
+ return $this->belongsTo(Ejaculation::class)->withLikes();
+ }
+}
diff --git a/app/MetadataResolver/ActivityPubResolver.php b/app/MetadataResolver/ActivityPubResolver.php
index 06328cb..7cf3e00 100644
--- a/app/MetadataResolver/ActivityPubResolver.php
+++ b/app/MetadataResolver/ActivityPubResolver.php
@@ -73,6 +73,10 @@ class ActivityPubResolver implements Resolver, Parser
private function html2text(string $html): string
{
+ if (empty($html)) {
+ return '';
+ }
+
$html = mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8');
$html = preg_replace('~
|
]*>~i', "\n", $html); $dom = new \DOMDocument(); diff --git a/app/MetadataResolver/MetadataResolver.php b/app/MetadataResolver/MetadataResolver.php index 181aab3..565e05f 100644 --- a/app/MetadataResolver/MetadataResolver.php +++ b/app/MetadataResolver/MetadataResolver.php @@ -13,7 +13,7 @@ class MetadataResolver implements Resolver '~nijie\.info/view(_popup)?\.php~' => NijieResolver::class, '~komiflo\.com(/#!)?/comics/(\\d+)~' => KomifloResolver::class, '~www\.melonbooks\.co\.jp/detail/detail\.php~' => MelonbooksResolver::class, - '~ec\.toranoana\.jp/tora_r/ec/item/.*~' => ToranoanaResolver::class, + '~ec\.toranoana\.(jp|shop)/(tora|joshi)(_[rd]+)?/(ec|digi)/item/~' => ToranoanaResolver::class, '~iwara\.tv/videos/.*~' => IwaraResolver::class, '~www\.dlsite\.com/.*/work/=/product_id/..\d+\.html~' => DLsiteResolver::class, '~dlsite\.jp/mawtw/..\d+~' => DLsiteResolver::class, diff --git a/app/User.php b/app/User.php index 5469e52..ec9c4d7 100644 --- a/app/User.php +++ b/app/User.php @@ -20,6 +20,7 @@ class User extends Authenticatable 'is_protected', 'accept_analytics', 'display_name', 'description', 'twitter_id', 'twitter_name', + 'private_likes', ]; /** @@ -51,4 +52,9 @@ class User extends Authenticatable { return Auth::check() && $this->id === Auth::user()->id; } + + public function likes() + { + return $this->hasMany(Like::class); + } } diff --git a/composer.json b/composer.json index 27bdca3..95dda47 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,8 @@ "guzzlehttp/guzzle": "^6.3", "laravel/framework": "5.5.*", "laravel/tinker": "~1.0", - "misd/linkify": "^1.1" + "misd/linkify": "^1.1", + "staudenmeir/eloquent-eager-limit": "^1.0" }, "require-dev": { "barryvdh/laravel-debugbar": "^3.1", @@ -50,6 +51,9 @@ ], "fix": [ "php-cs-fixer fix" + ], + "test": [ + "phpunit" ] }, "config": { diff --git a/composer.lock b/composer.lock index 6766e6b..344bbc1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "79423bebbfa31e28aab2d06ba7e19828", + "content-hash": "665f6f5eb180a1295fb60303d2ea5051", "packages": [ { "name": "anhskohbo/no-captcha", @@ -1873,6 +1873,48 @@ ], "time": "2018-07-19T23:38:55+00:00" }, + { + "name": "staudenmeir/eloquent-eager-limit", + "version": "v1.2", + "source": { + "type": "git", + "url": "https://github.com/staudenmeir/eloquent-eager-limit.git", + "reference": "4ee5c70268b5bc019618c7de66145d7addb3c5dc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staudenmeir/eloquent-eager-limit/zipball/4ee5c70268b5bc019618c7de66145d7addb3c5dc", + "reference": "4ee5c70268b5bc019618c7de66145d7addb3c5dc", + "shasum": "" + }, + "require": { + "illuminate/database": "~5.5.43|~5.6.34|5.7.*", + "illuminate/support": "^5.5.14", + "php": ">=7.0" + }, + "require-dev": { + "laravel/homestead": "^7.18", + "phpunit/phpunit": "~6.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Staudenmeir\\EloquentEagerLimit\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jonas Staudenmeir", + "email": "mail@jonas-staudenmeir.de" + } + ], + "description": "Laravel Eloquent eager loading with limit", + "time": "2019-01-23T19:25:27+00:00" + }, { "name": "swiftmailer/swiftmailer", "version": "v6.1.3", diff --git a/database/migrations/2019_03_26_224641_create_likes_table.php b/database/migrations/2019_03_26_224641_create_likes_table.php new file mode 100644 index 0000000..773dbd0 --- /dev/null +++ b/database/migrations/2019_03_26_224641_create_likes_table.php @@ -0,0 +1,37 @@ +increments('id'); + $table->integer('user_id')->index(); + $table->integer('ejaculation_id')->index(); + $table->timestamps(); + + $table->unique(['user_id', 'ejaculation_id']); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + $table->foreign('ejaculation_id')->references('id')->on('ejaculations')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('likes'); + } +} diff --git a/database/migrations/2019_04_09_224506_add_private_likes_to_users.php b/database/migrations/2019_04_09_224506_add_private_likes_to_users.php new file mode 100644 index 0000000..c2147bf --- /dev/null +++ b/database/migrations/2019_04_09_224506_add_private_likes_to_users.php @@ -0,0 +1,32 @@ +boolean('private_likes')->default(false); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('private_likes'); + }); + } +} diff --git a/resources/assets/js/app.js b/resources/assets/js/app.js index 6af9993..bf79d54 100644 --- a/resources/assets/js/app.js +++ b/resources/assets/js/app.js @@ -27,4 +27,69 @@ $(() => { event.preventDefault(); $deleteCheckinModal.modal('show', this); }); + + $(document).on('click', '[data-href]', function (event) { + location.href = $(this).data('href'); + }); + + $(document).on('click', '.like-button', function (event) { + event.preventDefault(); + + const $this = $(this); + const targetId = $this.data('id'); + const isLiked = $this.data('liked'); + + if (isLiked) { + const callback = (data) => { + $this.data('liked', false); + $this.find('.oi-heart').removeClass('text-danger'); + + const count = data.ejaculation ? data.ejaculation.likes_count : 0; + $this.find('.like-count').text(count ? count : ''); + }; + + $.ajax({ + url: '/api/likes/' + encodeURIComponent(targetId), + method: 'delete', + type: 'json' + }) + .then(callback) + .catch(function (xhr) { + if (xhr.status === 404) { + callback(JSON.parse(xhr.responseText)); + return; + } + + console.error(xhr); + alert('いいねを解除できませんでした。'); + }); + } else { + const callback = (data) => { + $this.data('liked', true); + $this.find('.oi-heart').addClass('text-danger'); + + const count = data.ejaculation ? data.ejaculation.likes_count : 0; + $this.find('.like-count').text(count ? count : ''); + }; + + $.ajax({ + url: '/api/likes', + method: 'post', + type: 'json', + data: { + id: targetId + } + }) + .then(callback) + .catch(function (xhr) { + if (xhr.status === 409) { + callback(JSON.parse(xhr.responseText)); + return; + } + + console.error(xhr); + alert('いいねできませんでした。'); + }); + } + }); }); \ No newline at end of file diff --git a/resources/assets/sass/app.scss b/resources/assets/sass/app.scss index e691a70..928bd95 100644 --- a/resources/assets/sass/app.scss +++ b/resources/assets/sass/app.scss @@ -12,4 +12,5 @@ $primary: #e53fb1; @import "tissue.css"; // Components -@import "components/link-card"; \ No newline at end of file +@import "components/ejaculation"; +@import "components/link-card"; diff --git a/resources/assets/sass/components/_ejaculation.scss b/resources/assets/sass/components/_ejaculation.scss new file mode 100644 index 0000000..dfe6e82 --- /dev/null +++ b/resources/assets/sass/components/_ejaculation.scss @@ -0,0 +1,21 @@ +.ejaculation-actions { + & > button:not(:last-child) { + margin-right: 24px; + } +} + +.like-button { + text-decoration: none !important; +} + +.like-count:not(:empty) { + padding-left: 0.5rem; +} + +.like-users { + height: 30px; +} + +.like-users-tall { + height: 36px; +} \ No newline at end of file diff --git a/resources/assets/sass/tissue.css b/resources/assets/sass/tissue.css index 2dd99e2..c8d5f15 100644 --- a/resources/assets/sass/tissue.css +++ b/resources/assets/sass/tissue.css @@ -40,10 +40,6 @@ border-top: none; } -.timeline-action-item { - margin-left: 16px; -} - .tis-global-count-graph { height: 90px; border-bottom: 1px solid rgba(0, 0, 0, .125); diff --git a/resources/views/components/ejaculation.blade.php b/resources/views/components/ejaculation.blade.php new file mode 100644 index 0000000..7b76bcf --- /dev/null +++ b/resources/views/components/ejaculation.blade.php @@ -0,0 +1,53 @@ + +
+ @foreach ($ejaculation->tags as $tag) + {{ $tag->name }} + @endforeach +
+@endif + +@if (!empty($ejaculation->link)) ++ {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} +
+@endif + +@if ($ejaculation->likes_count > 0) ++
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
@endif + + @if ($ejaculation->likes_count > 0) +- @foreach ($ejaculation->tags as $tag) - {{ $tag->name }} - @endforeach -
- @endif - - @if (!empty($ejaculation->link)) -- {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} -
- @endif + @component('components.ejaculation', compact('ejaculation')) + @endcomponent- @if ($ejaculation->is_private) - 非公開 - @endif - @foreach ($ejaculation->tags as $tag) - {{ $tag->name }} - @endforeach -
- @endif - - @if (!empty($ejaculation->link)) -- {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} -
- @endif + @component('components.ejaculation', compact('ejaculation')) + @endcomponent- @if ($ejaculation->is_private) - 非公開 - @endif - @foreach ($ejaculation->tags as $tag) - {{ $tag->name }} - @endforeach -
- @endif - - @if (!empty($ejaculation->link)) -- {!! Formatter::linkify(nl2br(e($ejaculation->note))) !!} -
- @endif + @component('components.ejaculation', compact('ejaculation')) + @endcomponent+ このユーザはいいね一覧を公開していません。 +
+@else +まだ何もいいと思ったことがありません。
++
{!! Formatter::linkify(nl2br(e($ejaculation->note))) !!}
@endif + + @if ($ejaculation->likes_count > 0) +