diff --git a/migrations/2019_09_28_222800_create_notification_preferences_table.php b/migrations/2019_09_28_222800_create_notification_preferences_table.php new file mode 100644 index 000000000..cd4a9839c --- /dev/null +++ b/migrations/2019_09_28_222800_create_notification_preferences_table.php @@ -0,0 +1,28 @@ + function (Builder $schema) { + $schema->create('notification_preferences', function (Blueprint $table) { + $table->integer('user_id')->unsigned(); + $table->string('type'); + $table->string('channel'); + $table->boolean('enabled')->default(false); + + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + }, + + 'down' => function (Builder $schema) { + $schema->drop('notification_preferences'); + } +]; diff --git a/migrations/2019_09_28_222800_create_users_table_preferences_columns.php b/migrations/2019_09_28_222800_create_users_table_preferences_columns.php new file mode 100644 index 000000000..bd0c17686 --- /dev/null +++ b/migrations/2019_09_28_222800_create_users_table_preferences_columns.php @@ -0,0 +1,27 @@ + function (Builder $schema) { + $schema->table('users', function (Blueprint $table) { + $table->boolean('disclose_online')->default(false); + $table->string('locale')->nullable(); + }); + }, + + 'down' => function (Builder $schema) { + $schema->table('users', function (Blueprint $table) { + $table->dropColumn('disclose_online'); + $table->dropColumn('locale'); + }); + } +]; diff --git a/migrations/2019_09_28_222801_migrate_users_table_preferences_columns.php b/migrations/2019_09_28_222801_migrate_users_table_preferences_columns.php new file mode 100644 index 000000000..5fcb77ef7 --- /dev/null +++ b/migrations/2019_09_28_222801_migrate_users_table_preferences_columns.php @@ -0,0 +1,52 @@ + function (Builder $builder) { + $db = $builder->getConnection(); + + $db->table('users') + ->select(['id', 'preferences']) + ->whereNotNull('preferences') + ->orderBy('id') + ->each(function ($user) use ($db) { + collect(json_decode($user->preferences ?? '{}')) + ->each(function ($value, $key) use ($user, $db) { + if (in_array($key, ['discloseOnline', 'followAfterReply'])) { + $db->table('users') + ->where('id', $user->id) + ->update([Str::snake($key) => (bool) $value]); + } + if ($key === 'locale') { + $db->table('users') + ->where('id', $user->id) + ->update(['locale' => $value]); + } + if (preg_match('/^notify_(?[^_]+)_(?.*)$/', $key, $matches)) { + $db->table('notification_preferences') + ->insert([ + 'user_id' => $user->id, + 'type' => $matches['type'], + 'channel' => $matches['channel'], + 'enabled' => (bool) $value + ]); + } + }); + }); + }, + + 'down' => function (Builder $builder) { + $db = $builder->getConnection(); + + $db->table('notification_preferences')->truncate(); + } +]; diff --git a/migrations/2019_09_28_222810_change_users_drop_preferences.php b/migrations/2019_09_28_222810_change_users_drop_preferences.php new file mode 100644 index 000000000..793f18fa8 --- /dev/null +++ b/migrations/2019_09_28_222810_change_users_drop_preferences.php @@ -0,0 +1,25 @@ + function (Builder $schema) { + $schema->table('users', function (Blueprint $table) { + $table->dropColumn('preferences'); + }); + }, + + 'down' => function (Builder $schema) { + $schema->table('users', function (Blueprint $table) { + $table->binary('preferences')->nullable(); + }); + } +]; diff --git a/src/User/Concerns/DeprecatedUserNotificationPreferences.php b/src/User/Concerns/DeprecatedUserNotificationPreferences.php new file mode 100644 index 000000000..0e36c54fb --- /dev/null +++ b/src/User/Concerns/DeprecatedUserNotificationPreferences.php @@ -0,0 +1,39 @@ +notificationPreferences->toArray(), array_keys(static::$preferences)); + + return array_merge($defaults, $user); + } + + /** + * Get the value of a preference for this user. + * + * @param string $key + * @param mixed $default + * @return mixed + * @deprecated 0.1.0-beta.13: `users.preferences` is no longer used. + */ + public function getPreference($key, $default = null) + { + return $this->$key ?? $default; + } + + /** + * Set the value of a preference for this user. + * + * @param string $key + * @param mixed $value + * @return $this + * @deprecated 0.1.0-beta.13: `users.preferences` is no longer used. + */ + public function setPreference($key, $value) + { + $preference = static::$preferences[$key]; + + // If a user preference is registered, transform the value. + if ($preference) { + $value = $value === null ? $preference['default'] : $value; + $value = $preference['transformer']($value); + } + + $this->{$key} = $value; + } + + /** + * Register a preference with a transformer and a default value. + * + * @param string $key + * @param callable $transformer + * @param mixed $default + */ + public static function addPreference($key, callable $transformer = null, $default = null) + { + static::$preferences[$key] = compact('transformer', 'default'); + } + +} diff --git a/src/User/NotificationPreference.php b/src/User/NotificationPreference.php new file mode 100644 index 000000000..ed5538987 --- /dev/null +++ b/src/User/NotificationPreference.php @@ -0,0 +1,59 @@ +belongsTo(User::class); + } + + public static function addChannel(string $channel) + { + static::$channels[] = $channel; + } + + public static function setNotificationPreference(User $user, string $type, string $channel, bool $enabled = true) + { + if (in_array($channel, static::$channels)) { + $attributes = [ + 'channel' => $channel, + 'type' => $type + ]; + + $user->notificationPreferences()->updateOrInsert($attributes, ['enabled' => $enabled]); + } else { + throw new InvalidArgumentException("Channel '$channel' is not registered."); + } + } + + public function scopeShouldBeNotified(Builder $query, string $type, string $channel = null) + { + return $query + ->where('enabled', true) + ->where('type', $type) + ->when($channel, function ($query, $channel) { + $query->where('channel', $channel); + }); + } +} diff --git a/src/User/User.php b/src/User/User.php index 197657447..ed8e97778 100644 --- a/src/User/User.php +++ b/src/User/User.php @@ -57,6 +57,8 @@ class User extends AbstractModel { use EventGeneratorTrait; use ScopeVisibilityTrait; + use Concerns\DeprecatedUserNotificationPreferences; + use Concerns\UserPreferences; /** * The attributes that should be mutated to dates. @@ -82,17 +84,6 @@ class User extends AbstractModel */ protected $session; - /** - * An array of registered user preferences. Each preference is defined with - * a key, and its value is an array containing the following keys:. - * - * - transformer: a callback that confines the value of the preference - * - default: a default value if the preference isn't set - * - * @var array - */ - protected static $preferences = []; - /** * The hasher with which to hash passwords. * @@ -446,34 +437,6 @@ class User extends AbstractModel })->count(); } - /** - * Get the values of all registered preferences for this user, by - * transforming their stored preferences and merging them with the defaults. - * - * @param string $value - * @return array - */ - public function getPreferencesAttribute($value) - { - $defaults = array_map(function ($value) { - return $value['default']; - }, static::$preferences); - - $user = Arr::only((array) json_decode($value, true), array_keys(static::$preferences)); - - return array_merge($defaults, $user); - } - - /** - * Encode an array of preferences for storage in the database. - * - * @param mixed $value - */ - public function setPreferencesAttribute($value) - { - $this->attributes['preferences'] = json_encode($value); - } - /** * Check whether or not the user should receive an alert for a notification * type. @@ -498,42 +461,6 @@ class User extends AbstractModel return (bool) $this->getPreference(static::getNotificationPreferenceKey($type, 'email')); } - /** - * Get the value of a preference for this user. - * - * @param string $key - * @param mixed $default - * @return mixed - */ - public function getPreference($key, $default = null) - { - return Arr::get($this->preferences, $key, $default); - } - - /** - * Set the value of a preference for this user. - * - * @param string $key - * @param mixed $value - * @return $this - */ - public function setPreference($key, $value) - { - if (isset(static::$preferences[$key])) { - $preferences = $this->preferences; - - if (! is_null($transformer = static::$preferences[$key]['transformer'])) { - $preferences[$key] = call_user_func($transformer, $value); - } else { - $preferences[$key] = $value; - } - - $this->preferences = $preferences; - } - - return $this; - } - /** * Set the user as being last seen just now. * @@ -606,6 +533,11 @@ class User extends AbstractModel return $this->belongsToMany(Group::class); } + public function notificationPreferences() + { + return $this->hasMany(NotificationPreference::class); + } + /** * Define the relationship with the user's notifications. * @@ -723,31 +655,6 @@ class User extends AbstractModel static::$hasher = $hasher; } - /** - * Register a preference with a transformer and a default value. - * - * @param string $key - * @param callable $transformer - * @param mixed $default - */ - public static function addPreference($key, callable $transformer = null, $default = null) - { - static::$preferences[$key] = compact('transformer', 'default'); - } - - /** - * Get the key for a preference which flags whether or not the user will - * receive a notification for $type via $method. - * - * @param string $type - * @param string $method - * @return string - */ - public static function getNotificationPreferenceKey($type, $method) - { - return 'notify_'.$type.'_'.$method; - } - /** * Refresh the user's comments count. *