cherry-pick emoji reaction changes
This commit is contained in:
parent
c633dd7d0f
commit
4750a8005d
|
@ -146,6 +146,7 @@ const excludeTypesFromFilter = filter => {
|
||||||
'follow',
|
'follow',
|
||||||
'follow_request',
|
'follow_request',
|
||||||
'favourite',
|
'favourite',
|
||||||
|
'reaction',
|
||||||
'reblog',
|
'reblog',
|
||||||
'mention',
|
'mention',
|
||||||
'poll',
|
'poll',
|
||||||
|
|
|
@ -755,6 +755,7 @@ class Status extends ImmutablePureComponent {
|
||||||
if (this.props.prepend && account) {
|
if (this.props.prepend && account) {
|
||||||
const notifKind = {
|
const notifKind = {
|
||||||
favourite: 'favourited',
|
favourite: 'favourited',
|
||||||
|
reaction: 'reacted',
|
||||||
reblog: 'boosted',
|
reblog: 'boosted',
|
||||||
reblogged_by: 'boosted',
|
reblogged_by: 'boosted',
|
||||||
status: 'posted',
|
status: 'posted',
|
||||||
|
|
|
@ -59,6 +59,14 @@ export default class StatusPrepend extends PureComponent {
|
||||||
values={{ name : link }}
|
values={{ name : link }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'reaction':
|
||||||
|
return (
|
||||||
|
<FormattedMessage
|
||||||
|
id='notification.reaction'
|
||||||
|
defaultMessage='{name} reacted to your post'
|
||||||
|
values={{ name: link }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return (
|
return (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
|
@ -113,6 +121,9 @@ export default class StatusPrepend extends PureComponent {
|
||||||
case 'favourite':
|
case 'favourite':
|
||||||
iconId = 'star';
|
iconId = 'star';
|
||||||
break;
|
break;
|
||||||
|
case 'reaction':
|
||||||
|
iconId = 'plus';
|
||||||
|
break;
|
||||||
case 'featured':
|
case 'featured':
|
||||||
iconId = 'thumb-tack';
|
iconId = 'thumb-tack';
|
||||||
break;
|
break;
|
||||||
|
|
|
@ -18,7 +18,6 @@ export default class StatusReactionsBar extends ImmutablePureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
statusId: PropTypes.string.isRequired,
|
statusId: PropTypes.string.isRequired,
|
||||||
reactions: ImmutablePropTypes.list.isRequired,
|
reactions: ImmutablePropTypes.list.isRequired,
|
||||||
reactionLimit: PropTypes.number.isRequired,
|
|
||||||
addReaction: PropTypes.func.isRequired,
|
addReaction: PropTypes.func.isRequired,
|
||||||
removeReaction: PropTypes.func.isRequired,
|
removeReaction: PropTypes.func.isRequired,
|
||||||
emojiMap: ImmutablePropTypes.map.isRequired,
|
emojiMap: ImmutablePropTypes.map.isRequired,
|
||||||
|
|
|
@ -120,6 +120,17 @@ export default class ColumnSettings extends PureComponent {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div role='group' aria-labelledby='notifications-reaction'>
|
||||||
|
<span id='notifications-reaction' className='column-settings__section'><FormattedMessage id='notifications.column_settings.reaction' defaultMessage='Reactions:' /></span>
|
||||||
|
|
||||||
|
<div className='column-settings__pillbar'>
|
||||||
|
<PillBarButton disabled={browserPermission === 'denied'} prefix='notifications_desktop' settings={settings} settingPath={['alerts', 'reaction']} onChange={onChange} label={alertStr} />
|
||||||
|
{showPushSettings && <PillBarButton prefix='notifications_push' settings={pushSettings} settingPath={['alerts', 'reaction']} onChange={this.onPushChange} label={pushStr} />}
|
||||||
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['shows', 'reaction']} onChange={onChange} label={showStr} />
|
||||||
|
<PillBarButton prefix='notifications' settings={settings} settingPath={['sounds', 'reaction']} onChange={onChange} label={soundStr} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div role='group' aria-labelledby='notifications-mention'>
|
<div role='group' aria-labelledby='notifications-mention'>
|
||||||
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
<span id='notifications-mention' className='column-settings__section'><FormattedMessage id='notifications.column_settings.mention' defaultMessage='Mentions:' /></span>
|
||||||
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ import { Icon } from 'flavours/glitch/components/icon';
|
||||||
const tooltips = defineMessages({
|
const tooltips = defineMessages({
|
||||||
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
mentions: { id: 'notifications.filter.mentions', defaultMessage: 'Mentions' },
|
||||||
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
|
favourites: { id: 'notifications.filter.favourites', defaultMessage: 'Favourites' },
|
||||||
|
reactions: { id: 'notifications.filter.reactions', defaultMessage: 'Reactions' },
|
||||||
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
boosts: { id: 'notifications.filter.boosts', defaultMessage: 'Boosts' },
|
||||||
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
polls: { id: 'notifications.filter.polls', defaultMessage: 'Poll results' },
|
||||||
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
follows: { id: 'notifications.filter.follows', defaultMessage: 'Follows' },
|
||||||
|
@ -75,6 +76,13 @@ class FilterBar extends PureComponent {
|
||||||
>
|
>
|
||||||
<Icon id='star' fixedWidth />
|
<Icon id='star' fixedWidth />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className={selectedFilter === 'reaction' ? 'active' : ''}
|
||||||
|
onClick={this.onClick('reaction')}
|
||||||
|
title={intl.formatMessage(tooltips.reactions)}
|
||||||
|
>
|
||||||
|
<Icon id='plus' fixedWidth />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className={selectedFilter === 'reblog' ? 'active' : ''}
|
className={selectedFilter === 'reblog' ? 'active' : ''}
|
||||||
onClick={this.onClick('reblog')}
|
onClick={this.onClick('reblog')}
|
||||||
|
|
|
@ -159,6 +159,28 @@ export default class Notification extends ImmutablePureComponent {
|
||||||
unread={this.props.unread}
|
unread={this.props.unread}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
case 'reaction':
|
||||||
|
return (
|
||||||
|
<StatusContainer
|
||||||
|
containerId={notification.get('id')}
|
||||||
|
hidden={hidden}
|
||||||
|
id={notification.get('status')}
|
||||||
|
account={notification.get('account')}
|
||||||
|
prepend='reaction'
|
||||||
|
muted
|
||||||
|
notification={notification}
|
||||||
|
onMoveDown={onMoveDown}
|
||||||
|
onMoveUp={onMoveUp}
|
||||||
|
onMention={onMention}
|
||||||
|
getScrollPosition={getScrollPosition}
|
||||||
|
updateScrollBottom={updateScrollBottom}
|
||||||
|
cachedMediaWidth={this.props.cachedMediaWidth}
|
||||||
|
cacheMediaWidth={this.props.cacheMediaWidth}
|
||||||
|
onUnmount={this.props.onUnmount}
|
||||||
|
withDismiss
|
||||||
|
unread={this.props.unread}
|
||||||
|
/>
|
||||||
|
);
|
||||||
case 'reblog':
|
case 'reblog':
|
||||||
return (
|
return (
|
||||||
<StatusContainer
|
<StatusContainer
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"keyboard_shortcuts.bookmark": "zu Lesezeichen hinzufügen",
|
"keyboard_shortcuts.bookmark": "zu Lesezeichen hinzufügen",
|
||||||
"keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden",
|
"keyboard_shortcuts.secondary_toot": "Toot mit sekundärer Privatsphäreeinstellung absenden",
|
||||||
"keyboard_shortcuts.toggle_collapse": "Toots ein-/ausklappen",
|
"keyboard_shortcuts.toggle_collapse": "Toots ein-/ausklappen",
|
||||||
|
"tooltips.reactions": "Reaktionen",
|
||||||
"layout.auto": "Automatisch",
|
"layout.auto": "Automatisch",
|
||||||
"layout.desktop": "Desktop",
|
"layout.desktop": "Desktop",
|
||||||
"layout.hint.auto": "Automatisch das Layout anhand der Einstellung \"Erweitertes Webinterface verwenden\" und Bildschirmgröße auswählen.",
|
"layout.hint.auto": "Automatisch das Layout anhand der Einstellung \"Erweitertes Webinterface verwenden\" und Bildschirmgröße auswählen.",
|
||||||
|
@ -74,6 +75,8 @@
|
||||||
"navigation_bar.keyboard_shortcuts": "Tastaturkürzel",
|
"navigation_bar.keyboard_shortcuts": "Tastaturkürzel",
|
||||||
"navigation_bar.misc": "Sonstiges",
|
"navigation_bar.misc": "Sonstiges",
|
||||||
"notification.markForDeletion": "Zum Entfernen auswählen",
|
"notification.markForDeletion": "Zum Entfernen auswählen",
|
||||||
|
"notification.reaction": "{name} hat auf deinen Beitrag reagiert",
|
||||||
|
"notifications.column_settings.reaction": "Reaktionen:",
|
||||||
"notification_purge.btn_all": "Alle\nauswählen",
|
"notification_purge.btn_all": "Alle\nauswählen",
|
||||||
"notification_purge.btn_apply": "Ausgewählte\nentfernen",
|
"notification_purge.btn_apply": "Ausgewählte\nentfernen",
|
||||||
"notification_purge.btn_invert": "Auswahl\numkehren",
|
"notification_purge.btn_invert": "Auswahl\numkehren",
|
||||||
|
|
|
@ -64,6 +64,7 @@
|
||||||
"keyboard_shortcuts.bookmark": "to bookmark",
|
"keyboard_shortcuts.bookmark": "to bookmark",
|
||||||
"keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting",
|
"keyboard_shortcuts.secondary_toot": "to send toot using secondary privacy setting",
|
||||||
"keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots",
|
"keyboard_shortcuts.toggle_collapse": "to collapse/uncollapse toots",
|
||||||
|
"tooltips.reactions": "Reactions",
|
||||||
"layout.auto": "Auto",
|
"layout.auto": "Auto",
|
||||||
"layout.desktop": "Desktop",
|
"layout.desktop": "Desktop",
|
||||||
"layout.hint.auto": "Automatically chose layout based on “Enable advanced web interface” setting and screen size.",
|
"layout.hint.auto": "Automatically chose layout based on “Enable advanced web interface” setting and screen size.",
|
||||||
|
@ -77,6 +78,8 @@
|
||||||
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
"navigation_bar.keyboard_shortcuts": "Keyboard shortcuts",
|
||||||
"navigation_bar.misc": "Misc",
|
"navigation_bar.misc": "Misc",
|
||||||
"notification.markForDeletion": "Mark for deletion",
|
"notification.markForDeletion": "Mark for deletion",
|
||||||
|
"notification.reaction": "{name} reacted to your post",
|
||||||
|
"notifications.column_settings.reaction": "Reactions:",
|
||||||
"notification_purge.btn_all": "Select\nall",
|
"notification_purge.btn_all": "Select\nall",
|
||||||
"notification_purge.btn_apply": "Clear\nselected",
|
"notification_purge.btn_apply": "Clear\nselected",
|
||||||
"notification_purge.btn_invert": "Invert\nselection",
|
"notification_purge.btn_invert": "Invert\nselection",
|
||||||
|
|
|
@ -61,6 +61,7 @@
|
||||||
"keyboard_shortcuts.bookmark": "ajouter aux marque-pages",
|
"keyboard_shortcuts.bookmark": "ajouter aux marque-pages",
|
||||||
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
|
"keyboard_shortcuts.secondary_toot": "Envoyer le post en utilisant les paramètres secondaires de confidentialité",
|
||||||
"keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts",
|
"keyboard_shortcuts.toggle_collapse": "Plier/déplier les posts",
|
||||||
|
"tooltips.reactions": "Réactions",
|
||||||
"layout.auto": "Auto",
|
"layout.auto": "Auto",
|
||||||
"layout.desktop": "Ordinateur",
|
"layout.desktop": "Ordinateur",
|
||||||
"layout.hint.auto": "Choisir automatiquement la mise en page selon l'option \"Activer l'interface Web avancée\" et la taille d'écran.",
|
"layout.hint.auto": "Choisir automatiquement la mise en page selon l'option \"Activer l'interface Web avancée\" et la taille d'écran.",
|
||||||
|
@ -74,6 +75,8 @@
|
||||||
"navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
|
"navigation_bar.keyboard_shortcuts": "Raccourcis clavier",
|
||||||
"navigation_bar.misc": "Autres",
|
"navigation_bar.misc": "Autres",
|
||||||
"notification.markForDeletion": "Ajouter aux éléments à supprimer",
|
"notification.markForDeletion": "Ajouter aux éléments à supprimer",
|
||||||
|
"notification.reaction": "{name} a réagi·e à votre message",
|
||||||
|
"notifications.column_settings.reaction": "Réactions:",
|
||||||
"notification_purge.btn_all": "Sélectionner\ntout",
|
"notification_purge.btn_all": "Sélectionner\ntout",
|
||||||
"notification_purge.btn_apply": "Effacer\nla sélection",
|
"notification_purge.btn_apply": "Effacer\nla sélection",
|
||||||
"notification_purge.btn_invert": "Inverser\nla sélection",
|
"notification_purge.btn_invert": "Inverser\nla sélection",
|
||||||
|
|
|
@ -39,6 +39,7 @@ const initialState = ImmutableMap({
|
||||||
follow: false,
|
follow: false,
|
||||||
follow_request: false,
|
follow_request: false,
|
||||||
favourite: false,
|
favourite: false,
|
||||||
|
reaction: false,
|
||||||
reblog: false,
|
reblog: false,
|
||||||
mention: false,
|
mention: false,
|
||||||
poll: false,
|
poll: false,
|
||||||
|
@ -62,6 +63,7 @@ const initialState = ImmutableMap({
|
||||||
follow_request: false,
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
|
reaction: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
poll: true,
|
poll: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
@ -75,6 +77,7 @@ const initialState = ImmutableMap({
|
||||||
follow_request: false,
|
follow_request: false,
|
||||||
favourite: true,
|
favourite: true,
|
||||||
reblog: true,
|
reblog: true,
|
||||||
|
reaction: true,
|
||||||
mention: true,
|
mention: true,
|
||||||
poll: true,
|
poll: true,
|
||||||
status: true,
|
status: true,
|
||||||
|
|
|
@ -25,6 +25,7 @@ class Notification < ApplicationRecord
|
||||||
'Follow' => :follow,
|
'Follow' => :follow,
|
||||||
'FollowRequest' => :follow_request,
|
'FollowRequest' => :follow_request,
|
||||||
'Favourite' => :favourite,
|
'Favourite' => :favourite,
|
||||||
|
'StatusReaction' => :reaction,
|
||||||
'Poll' => :poll,
|
'Poll' => :poll,
|
||||||
}.freeze
|
}.freeze
|
||||||
|
|
||||||
|
@ -35,6 +36,7 @@ class Notification < ApplicationRecord
|
||||||
follow
|
follow
|
||||||
follow_request
|
follow_request
|
||||||
favourite
|
favourite
|
||||||
|
reaction
|
||||||
poll
|
poll
|
||||||
update
|
update
|
||||||
admin.sign_up
|
admin.sign_up
|
||||||
|
@ -46,6 +48,7 @@ class Notification < ApplicationRecord
|
||||||
reblog: [status: :reblog],
|
reblog: [status: :reblog],
|
||||||
mention: [mention: :status],
|
mention: [mention: :status],
|
||||||
favourite: [favourite: :status],
|
favourite: [favourite: :status],
|
||||||
|
reaction: [status_reaction: :status],
|
||||||
poll: [poll: :status],
|
poll: [poll: :status],
|
||||||
update: :status,
|
update: :status,
|
||||||
'admin.report': [report: :target_account],
|
'admin.report': [report: :target_account],
|
||||||
|
@ -61,6 +64,7 @@ class Notification < ApplicationRecord
|
||||||
belongs_to :follow, inverse_of: :notification
|
belongs_to :follow, inverse_of: :notification
|
||||||
belongs_to :follow_request, inverse_of: :notification
|
belongs_to :follow_request, inverse_of: :notification
|
||||||
belongs_to :favourite, inverse_of: :notification
|
belongs_to :favourite, inverse_of: :notification
|
||||||
|
belongs_to :status_reaction, inverse_of: :notification
|
||||||
belongs_to :poll, inverse_of: false
|
belongs_to :poll, inverse_of: false
|
||||||
belongs_to :report, inverse_of: false
|
belongs_to :report, inverse_of: false
|
||||||
end
|
end
|
||||||
|
@ -81,6 +85,8 @@ class Notification < ApplicationRecord
|
||||||
status&.reblog
|
status&.reblog
|
||||||
when :favourite
|
when :favourite
|
||||||
favourite&.status
|
favourite&.status
|
||||||
|
when :reaction
|
||||||
|
status_reaction&.status
|
||||||
when :mention
|
when :mention
|
||||||
mention&.status
|
mention&.status
|
||||||
when :poll
|
when :poll
|
||||||
|
@ -130,6 +136,8 @@ class Notification < ApplicationRecord
|
||||||
notification.status.reblog = cached_status
|
notification.status.reblog = cached_status
|
||||||
when :favourite
|
when :favourite
|
||||||
notification.favourite.status = cached_status
|
notification.favourite.status = cached_status
|
||||||
|
when :reaction
|
||||||
|
notification.reaction.status = cached_status
|
||||||
when :mention
|
when :mention
|
||||||
notification.mention.status = cached_status
|
notification.mention.status = cached_status
|
||||||
when :poll
|
when :poll
|
||||||
|
@ -141,6 +149,8 @@ class Notification < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
alias reaction status_reaction
|
||||||
|
|
||||||
after_initialize :set_from_account
|
after_initialize :set_from_account
|
||||||
before_validation :set_from_account
|
before_validation :set_from_account
|
||||||
|
|
||||||
|
@ -150,7 +160,7 @@ class Notification < ApplicationRecord
|
||||||
return unless new_record?
|
return unless new_record?
|
||||||
|
|
||||||
case activity_type
|
case activity_type
|
||||||
when 'Status', 'Follow', 'Favourite', 'FollowRequest', 'Poll', 'Report'
|
when 'Status', 'Follow', 'Favourite', 'StatusReaction', 'FollowRequest', 'Poll', 'Report'
|
||||||
self.from_account_id = activity&.account_id
|
self.from_account_id = activity&.account_id
|
||||||
when 'Mention'
|
when 'Mention'
|
||||||
self.from_account_id = activity&.status&.account_id
|
self.from_account_id = activity&.status&.account_id
|
||||||
|
|
|
@ -12,7 +12,7 @@ class REST::NotificationSerializer < ActiveModel::Serializer
|
||||||
end
|
end
|
||||||
|
|
||||||
def status_type?
|
def status_type?
|
||||||
[:favourite, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
[:favourite, :reaction, :reblog, :status, :mention, :poll, :update].include?(object.type)
|
||||||
end
|
end
|
||||||
|
|
||||||
def report_type?
|
def report_type?
|
||||||
|
|
|
@ -14,6 +14,7 @@ class StatusReactionService < BaseService
|
||||||
|
|
||||||
json = Oj.dump(serialize_payload(reaction, ActivityPub::EmojiReactionSerializer))
|
json = Oj.dump(serialize_payload(reaction, ActivityPub::EmojiReactionSerializer))
|
||||||
if status.account.local?
|
if status.account.local?
|
||||||
|
NotifyService.new.call(status.account, :reaction, reaction)
|
||||||
ActivityPub::RawDistributionWorker.perform_async(json, status.account.id)
|
ActivityPub::RawDistributionWorker.perform_async(json, status.account.id)
|
||||||
else
|
else
|
||||||
ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url)
|
ActivityPub::DeliveryWorker.perform_async(json, reaction.account_id, status.account.inbox_url)
|
||||||
|
|
|
@ -1339,6 +1339,10 @@ de:
|
||||||
body: 'Dein Beitrag wurde von %{name} favorisiert:'
|
body: 'Dein Beitrag wurde von %{name} favorisiert:'
|
||||||
subject: "%{name} favorisierte deinen Beitrag"
|
subject: "%{name} favorisierte deinen Beitrag"
|
||||||
title: Neue Favorisierung
|
title: Neue Favorisierung
|
||||||
|
reaction:
|
||||||
|
body: '%{name} hat auf deinen Beitrag reagiert:'
|
||||||
|
subject: '%{name} hat auf deinen Beitrag reagiert'
|
||||||
|
title: Neue Reaktion
|
||||||
follow:
|
follow:
|
||||||
body: "%{name} folgt dir jetzt!"
|
body: "%{name} folgt dir jetzt!"
|
||||||
subject: "%{name} folgt dir jetzt"
|
subject: "%{name} folgt dir jetzt"
|
||||||
|
|
|
@ -1433,6 +1433,10 @@ en:
|
||||||
title: New mention
|
title: New mention
|
||||||
poll:
|
poll:
|
||||||
subject: A poll by %{name} has ended
|
subject: A poll by %{name} has ended
|
||||||
|
reaction:
|
||||||
|
body: '%{name} reacted to your post:'
|
||||||
|
subject: '%{name} reacted to your post'
|
||||||
|
title: New reaction
|
||||||
reblog:
|
reblog:
|
||||||
body: 'Your post was boosted by %{name}:'
|
body: 'Your post was boosted by %{name}:'
|
||||||
subject: "%{name} boosted your post"
|
subject: "%{name} boosted your post"
|
||||||
|
|
|
@ -1323,6 +1323,10 @@ fr:
|
||||||
body: "%{name} a ajouté votre message à ses favoris :"
|
body: "%{name} a ajouté votre message à ses favoris :"
|
||||||
subject: "%{name} a ajouté votre message à ses favoris"
|
subject: "%{name} a ajouté votre message à ses favoris"
|
||||||
title: Nouveau favori
|
title: Nouveau favori
|
||||||
|
reaction:
|
||||||
|
body: '%{name} a réagi·e à votre message:'
|
||||||
|
subject: '%{name} a réagi·e à votre message'
|
||||||
|
title: Nouvelle réaction
|
||||||
follow:
|
follow:
|
||||||
body: "%{name} vous suit !"
|
body: "%{name} vous suit !"
|
||||||
subject: "%{name} vous suit"
|
subject: "%{name} vous suit"
|
||||||
|
|
Reference in New Issue