diff --git a/Gemfile b/Gemfile index 042db3a555..3949c0f4dc 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'puma', '~> 4.3' gem 'rails', '~> 5.2.4' gem 'sprockets', '~> 3.7.2' gem 'thor', '~> 0.20' +gem 'rack', '2.0.8' gem 'thwait', '~> 0.1.0' gem 'e2mmap', '~> 0.1.0' diff --git a/Gemfile.lock b/Gemfile.lock index 8394b7baaa..bfc0d5c552 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -443,7 +443,7 @@ GEM pundit (2.1.0) activesupport (>= 3.0.0) raabro (1.1.6) - rack (2.1.1) + rack (2.0.8) rack-attack (6.2.2) rack (>= 1.0, < 3) rack-cors (1.1.1) @@ -753,6 +753,7 @@ DEPENDENCIES pry-rails (~> 0.3) puma (~> 4.3) pundit (~> 2.1) + rack (= 2.0.8) rack-attack (~> 6.2) rack-cors (~> 1.1) rails (~> 5.2.4) diff --git a/app/controllers/admin/announcements_controller.rb b/app/controllers/admin/announcements_controller.rb index 02198f0b52..494fd13d0f 100644 --- a/app/controllers/admin/announcements_controller.rb +++ b/app/controllers/admin/announcements_controller.rb @@ -20,8 +20,9 @@ class Admin::AnnouncementsController < Admin::BaseController @announcement = Announcement.new(resource_params) if @announcement.save + PublishScheduledAnnouncementWorker.perform_async(@announcement.id) if @announcement.published? log_action :create, @announcement - redirect_to admin_announcements_path + redirect_to admin_announcements_path, notice: @announcement.published? ? I18n.t('admin.announcements.published_msg') : I18n.t('admin.announcements.scheduled_msg') else render :new end @@ -35,18 +36,36 @@ class Admin::AnnouncementsController < Admin::BaseController authorize :announcement, :update? if @announcement.update(resource_params) + PublishScheduledAnnouncementWorker.perform_async(@announcement.id) if @announcement.published? log_action :update, @announcement - redirect_to admin_announcements_path + redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.updated_msg') else render :edit end end + def publish + authorize :announcement, :update? + @announcement.publish! + PublishScheduledAnnouncementWorker.perform_async(@announcement.id) + log_action :update, @announcement + redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.published_msg') + end + + def unpublish + authorize :announcement, :update? + @announcement.unpublish! + UnpublishAnnouncementWorker.perform_async(@announcement.id) + log_action :update, @announcement + redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.unpublished_msg') + end + def destroy authorize :announcement, :destroy? @announcement.destroy! + UnpublishAnnouncementWorker.perform_async(@announcement.id) if @announcement.published? log_action :destroy, @announcement - redirect_to admin_announcements_path + redirect_to admin_announcements_path, notice: I18n.t('admin.announcements.destroyed_msg') end private diff --git a/app/helpers/settings_helper.rb b/app/helpers/settings_helper.rb index 10aaf3acaa..825aa974d8 100644 --- a/app/helpers/settings_helper.rb +++ b/app/helpers/settings_helper.rb @@ -36,6 +36,7 @@ module SettingsHelper it: 'Italiano', ja: '日本語', ka: 'ქართული', + kab: 'Taqbaylit', kk: 'Қазақша', kn: 'ಕನ್ನಡ', ko: '한국어', diff --git a/app/javascript/mastodon/actions/announcements.js b/app/javascript/mastodon/actions/announcements.js index 53b19a6fda..f072a407f9 100644 --- a/app/javascript/mastodon/actions/announcements.js +++ b/app/javascript/mastodon/actions/announcements.js @@ -5,6 +5,7 @@ export const ANNOUNCEMENTS_FETCH_REQUEST = 'ANNOUNCEMENTS_FETCH_REQUEST'; export const ANNOUNCEMENTS_FETCH_SUCCESS = 'ANNOUNCEMENTS_FETCH_SUCCESS'; export const ANNOUNCEMENTS_FETCH_FAIL = 'ANNOUNCEMENTS_FETCH_FAIL'; export const ANNOUNCEMENTS_UPDATE = 'ANNOUNCEMENTS_UPDATE'; +export const ANNOUNCEMENTS_DELETE = 'ANNOUNCEMENTS_DELETE'; export const ANNOUNCEMENTS_REACTION_ADD_REQUEST = 'ANNOUNCEMENTS_REACTION_ADD_REQUEST'; export const ANNOUNCEMENTS_REACTION_ADD_SUCCESS = 'ANNOUNCEMENTS_REACTION_ADD_SUCCESS'; @@ -139,8 +140,11 @@ export const updateReaction = reaction => ({ reaction, }); -export function toggleShowAnnouncements() { - return dispatch => { - dispatch({ type: ANNOUNCEMENTS_TOGGLE_SHOW }); - }; -} +export const toggleShowAnnouncements = () => ({ + type: ANNOUNCEMENTS_TOGGLE_SHOW, +}); + +export const deleteAnnouncement = id => ({ + type: ANNOUNCEMENTS_DELETE, + id, +}); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index ac325f74cd..79b08bddac 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -8,7 +8,12 @@ import { } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; -import { fetchAnnouncements, updateAnnouncements, updateReaction as updateAnnouncementsReaction } from './announcements'; +import { + fetchAnnouncements, + updateAnnouncements, + updateReaction as updateAnnouncementsReaction, + deleteAnnouncement, +} from './announcements'; import { fetchFilters } from './filters'; import { getLocale } from '../locales'; @@ -51,6 +56,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, case 'announcement.reaction': dispatch(updateAnnouncementsReaction(JSON.parse(data.payload))); break; + case 'announcement.delete': + dispatch(deleteAnnouncement(data.payload)); + break; } }, }; diff --git a/app/javascript/mastodon/components/animated_number.js b/app/javascript/mastodon/components/animated_number.js index 2d359fbc60..f3127c88ef 100644 --- a/app/javascript/mastodon/components/animated_number.js +++ b/app/javascript/mastodon/components/animated_number.js @@ -11,23 +11,41 @@ export default class AnimatedNumber extends React.PureComponent { value: PropTypes.number.isRequired, }; - willEnter () { - return { y: -1 }; + state = { + direction: 1, + }; + + componentWillReceiveProps (nextProps) { + if (nextProps.value > this.props.value) { + this.setState({ direction: 1 }); + } else if (nextProps.value < this.props.value) { + this.setState({ direction: -1 }); + } } - willLeave () { - return { y: spring(1, { damping: 35, stiffness: 400 }) }; + willEnter = () => { + const { direction } = this.state; + + return { y: -1 * direction }; + } + + willLeave = () => { + const { direction } = this.state; + + return { y: spring(1 * direction, { damping: 35, stiffness: 400 }) }; } render () { const { value } = this.props; + const { direction } = this.state; if (reduceMotion) { return ; } const styles = [{ - key: value, + key: `${value}`, + data: value, style: { y: spring(0, { damping: 35, stiffness: 400 }) }, }]; @@ -35,8 +53,8 @@ export default class AnimatedNumber extends React.PureComponent { {items => ( - {items.map(({ key, style }) => ( - 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> + {items.map(({ key, data, style }) => ( + 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}> ))} )} diff --git a/app/javascript/mastodon/components/relative_timestamp.js b/app/javascript/mastodon/components/relative_timestamp.js index aa4b73cfe6..711181dcdb 100644 --- a/app/javascript/mastodon/components/relative_timestamp.js +++ b/app/javascript/mastodon/components/relative_timestamp.js @@ -3,6 +3,7 @@ import { injectIntl, defineMessages } from 'react-intl'; import PropTypes from 'prop-types'; const messages = defineMessages({ + today: { id: 'relative_time.today', defaultMessage: 'today' }, just_now: { id: 'relative_time.just_now', defaultMessage: 'now' }, seconds: { id: 'relative_time.seconds', defaultMessage: '{number}s' }, minutes: { id: 'relative_time.minutes', defaultMessage: '{number}m' }, @@ -65,12 +66,14 @@ const getUnitDelay = units => { } }; -export const timeAgoString = (intl, date, now, year) => { +export const timeAgoString = (intl, date, now, year, timeGiven = true) => { const delta = now - date.getTime(); let relativeTime; - if (delta < 10 * SECOND) { + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.just_now); } else if (delta < 7 * DAY) { if (delta < MINUTE) { @@ -91,12 +94,14 @@ export const timeAgoString = (intl, date, now, year) => { return relativeTime; }; -const timeRemainingString = (intl, date, now) => { +const timeRemainingString = (intl, date, now, timeGiven = true) => { const delta = date.getTime() - now; let relativeTime; - if (delta < 10 * SECOND) { + if (delta < DAY && !timeGiven) { + relativeTime = intl.formatMessage(messages.today); + } else if (delta < 10 * SECOND) { relativeTime = intl.formatMessage(messages.moments_remaining); } else if (delta < MINUTE) { relativeTime = intl.formatMessage(messages.seconds_remaining, { number: Math.floor(delta / SECOND) }); @@ -173,8 +178,9 @@ class RelativeTimestamp extends React.Component { render () { const { timestamp, intl, year, futureDate } = this.props; + const timeGiven = timestamp.includes('T'); const date = new Date(timestamp); - const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now) : timeAgoString(intl, date, this.state.now, year); + const relativeTime = futureDate ? timeRemainingString(intl, date, this.state.now, timeGiven) : timeAgoString(intl, date, this.state.now, year, timeGiven); return (