Improve in-app notifications design (#561)
This commit is contained in:
parent
26c365a00b
commit
516d184916
9 changed files with 230 additions and 39 deletions
|
|
@ -37,7 +37,7 @@ export default class Root extends PureComponent {
|
|||
navigator.showInAppNotification({
|
||||
screen: 'Notification',
|
||||
position: 'top',
|
||||
autoDismissTimerSec: 15,
|
||||
autoDismissTimerSec: 5,
|
||||
dismissWithSwipe: true,
|
||||
passProps: {
|
||||
notification
|
||||
|
|
|
|||
|
|
@ -128,6 +128,11 @@ export default class Mattermost {
|
|||
notification = {
|
||||
data: {
|
||||
channel_id: deviceNotification.channel_id,
|
||||
channel_name: deviceNotification.channel_name,
|
||||
sender_id: deviceNotification.sender_id,
|
||||
override_username: deviceNotification.override_username,
|
||||
override_icon_url: deviceNotification.override_icon_url,
|
||||
from_webhook: deviceNotification.from_webhook,
|
||||
team_id: deviceNotification.team_id
|
||||
},
|
||||
message
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export function registerScreens(store, Provider) {
|
|||
Navigation.registerComponent('MoreChannels', () => wrapWithContextProvider(MoreChannels), store, Provider);
|
||||
Navigation.registerComponent('MoreDirectMessages', () => wrapWithContextProvider(MoreDirectMessages), store, Provider);
|
||||
Navigation.registerComponent('OptionsModal', () => wrapWithContextProvider(OptionsModal), store, Provider);
|
||||
Navigation.registerComponent('Notification', () => Notification, store, Provider);
|
||||
Navigation.registerComponent('Notification', () => wrapWithContextProvider(Notification), store, Provider);
|
||||
Navigation.registerComponent('Root', () => Root, store, Provider);
|
||||
Navigation.registerComponent('SelectServer', () => wrapWithContextProvider(SelectServer), store, Provider);
|
||||
Navigation.registerComponent('SelectTeam', () => wrapWithContextProvider(SelectTeam), store, Provider);
|
||||
|
|
|
|||
|
|
@ -7,11 +7,31 @@ import {connect} from 'react-redux';
|
|||
import {goToNotification} from 'app/actions/views/root';
|
||||
import {getTheme} from 'app/selectors/preferences';
|
||||
|
||||
import {getChannel} from 'mattermost-redux/selectors/entities/channels';
|
||||
import {getMyPreferences} from 'mattermost-redux/selectors/entities/preferences';
|
||||
import {getUser} from 'mattermost-redux/selectors/entities/users';
|
||||
|
||||
import Notification from './notification';
|
||||
|
||||
function mapStateToProps(state, ownProps) {
|
||||
const {data} = ownProps.notification;
|
||||
|
||||
let user;
|
||||
if (data.sender_id) {
|
||||
user = getUser(state, data.sender_id);
|
||||
}
|
||||
|
||||
let channel;
|
||||
if (data.channel_id) {
|
||||
channel = getChannel(state, data.channel_id);
|
||||
}
|
||||
|
||||
return {
|
||||
...ownProps,
|
||||
config: state.entities.general.config,
|
||||
channel,
|
||||
user,
|
||||
myPreferences: getMyPreferences(state),
|
||||
theme: getTheme(state)
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,18 +13,30 @@ import {
|
|||
View
|
||||
} from 'react-native';
|
||||
|
||||
import FormattedText from 'app/components/formatted_text';
|
||||
import ProfilePicture from 'app/components/profile_picture';
|
||||
import {changeOpacity} from 'app/utils/theme';
|
||||
|
||||
import icon from 'assets/images/icon.png';
|
||||
import {isDirectChannel} from 'mattermost-redux/utils/channel_utils';
|
||||
import {displayUsername} from 'mattermost-redux/utils/user_utils';
|
||||
|
||||
import logo from 'assets/images/icon.png';
|
||||
import webhookIcon from 'assets/images/icons/webhook.jpg';
|
||||
|
||||
const IMAGE_SIZE = 33;
|
||||
|
||||
export default class Notification extends PureComponent {
|
||||
static propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
goToNotification: PropTypes.func.isRequired
|
||||
}).isRequired,
|
||||
channel: PropTypes.object,
|
||||
config: PropTypes.object,
|
||||
notification: PropTypes.object.isRequired,
|
||||
myPreferences: PropTypes.object.isRequired,
|
||||
navigator: PropTypes.object,
|
||||
theme: PropTypes.object.isRequired
|
||||
theme: PropTypes.object.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
notificationTapped = () => {
|
||||
|
|
@ -46,57 +58,209 @@ export default class Notification extends PureComponent {
|
|||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<TouchableOpacity
|
||||
style={{flex: 1, flexDirection: 'row'}}
|
||||
onPress={this.notificationTapped}
|
||||
>
|
||||
<View>
|
||||
<Image
|
||||
source={icon}
|
||||
style={style.icon}
|
||||
/>
|
||||
</View>
|
||||
<View style={{flex: 1, flexDirection: 'row', alignSelf: 'stretch', justifyContent: 'flex-start', marginLeft: 10}} >
|
||||
<Text
|
||||
numberOfLines={2}
|
||||
style={style.message}
|
||||
>
|
||||
{this.props.notification.message}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
getNotificationIcon = () => {
|
||||
const {config, notification, user} = this.props;
|
||||
const {data} = notification;
|
||||
|
||||
let icon = (
|
||||
<Image
|
||||
source={logo}
|
||||
style={style.icon}
|
||||
/>
|
||||
);
|
||||
|
||||
if (data.from_webhook && config.EnablePostIconOverride === 'true') {
|
||||
const wsIcon = data.override_icon_url ? {uri: data.override_icon_url} : webhookIcon;
|
||||
icon = (
|
||||
<Image
|
||||
source={wsIcon}
|
||||
style={style.icon}
|
||||
/>
|
||||
);
|
||||
} else if (user) {
|
||||
icon = (
|
||||
<ProfilePicture
|
||||
user={user}
|
||||
size={IMAGE_SIZE}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return icon;
|
||||
};
|
||||
|
||||
getNotificationTitle = (titleText) => {
|
||||
const {channel} = this.props;
|
||||
|
||||
let title = (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={style.title}
|
||||
>
|
||||
{titleText}
|
||||
</Text>
|
||||
);
|
||||
|
||||
const userName = this.getNotificationUserName();
|
||||
if (userName && channel) {
|
||||
let channelName;
|
||||
let inText;
|
||||
if (!isDirectChannel(channel)) {
|
||||
inText = (
|
||||
<FormattedText
|
||||
id='mobile.notification.in'
|
||||
defaultMessage=' in '
|
||||
style={style.message}
|
||||
/>
|
||||
);
|
||||
|
||||
channelName = (
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={[style.title, style.channelName]}
|
||||
>
|
||||
{channel.display_name}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
title = (
|
||||
<View style={{flex: 1, flexDirection: 'row'}}>
|
||||
{userName}
|
||||
{inText}
|
||||
{channelName}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return title;
|
||||
};
|
||||
|
||||
getNotificationUserName = () => {
|
||||
const {config, notification, myPreferences, user} = this.props;
|
||||
const {data} = notification;
|
||||
|
||||
let userName;
|
||||
if (data.override_username && config.EnablePostUsernameOverride === 'true') {
|
||||
userName = (
|
||||
<Text style={style.title}>
|
||||
{data.override_username}
|
||||
</Text>
|
||||
);
|
||||
} else if (user) {
|
||||
userName = (
|
||||
<Text style={style.title}>
|
||||
{displayUsername(user, myPreferences)}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
return userName;
|
||||
};
|
||||
|
||||
render() {
|
||||
const {message} = this.props.notification;
|
||||
|
||||
if (message) {
|
||||
const msg = message.split(':');
|
||||
const titleText = msg.shift();
|
||||
const messageText = msg.join('').trim();
|
||||
|
||||
const title = this.getNotificationTitle(titleText);
|
||||
const icon = this.getNotificationIcon();
|
||||
|
||||
return (
|
||||
<View style={style.container}>
|
||||
<TouchableOpacity
|
||||
style={{flex: 1, flexDirection: 'row'}}
|
||||
onPress={this.notificationTapped}
|
||||
>
|
||||
<View style={style.iconContainer}>
|
||||
{icon}
|
||||
</View>
|
||||
<View style={style.textContainer}>
|
||||
{title}
|
||||
<View style={{flex: 1}}>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
ellipsizeMode='tail'
|
||||
style={style.message}
|
||||
>
|
||||
{messageText}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const style = StyleSheet.create({
|
||||
container: {
|
||||
alignItems: 'center',
|
||||
alignItems: 'flex-start',
|
||||
backgroundColor: changeOpacity('#000', 0.9),
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
padding: 10,
|
||||
paddingHorizontal: 10,
|
||||
width: Dimensions.get('window').width,
|
||||
...Platform.select({
|
||||
android: {
|
||||
height: 60
|
||||
height: 68
|
||||
},
|
||||
ios: {
|
||||
height: 80
|
||||
height: 88
|
||||
}
|
||||
})
|
||||
},
|
||||
iconContainer: {
|
||||
...Platform.select({
|
||||
android: {
|
||||
paddingTop: 17
|
||||
},
|
||||
ios: {
|
||||
paddingTop: 37
|
||||
}
|
||||
})
|
||||
},
|
||||
icon: {
|
||||
borderRadius: 15,
|
||||
height: 30,
|
||||
width: 30
|
||||
borderRadius: (IMAGE_SIZE / 2),
|
||||
height: IMAGE_SIZE,
|
||||
width: IMAGE_SIZE
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
alignSelf: 'stretch',
|
||||
alignItems: 'flex-start',
|
||||
marginLeft: 10,
|
||||
...Platform.select({
|
||||
android: {
|
||||
marginTop: 17,
|
||||
height: 50
|
||||
},
|
||||
ios: {
|
||||
paddingTop: 37
|
||||
}
|
||||
})
|
||||
},
|
||||
title: {
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14,
|
||||
fontWeight: '600'
|
||||
},
|
||||
channelName: {
|
||||
alignSelf: 'stretch',
|
||||
alignItems: 'flex-start',
|
||||
flex: 1
|
||||
},
|
||||
message: {
|
||||
color: 'white',
|
||||
fontSize: 13
|
||||
color: '#FFFFFF',
|
||||
fontSize: 14
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export default class SelectTeam extends PureComponent {
|
|||
/>
|
||||
<FormattedText
|
||||
style={GlobalStyles.subheader}
|
||||
id='signup_team.choose'
|
||||
id='mobile.select_team.choose'
|
||||
defaultMessage='Your teams:'
|
||||
/>
|
||||
{content}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ class Settings extends PureComponent {
|
|||
{showTeamSelection &&
|
||||
<SettingsItem
|
||||
defaultMessage='Team Selection'
|
||||
i18nId='sidebar_right_menu.switch_team'
|
||||
i18nId='mobile.settings.team_selection'
|
||||
iconName='ios-people'
|
||||
iconType='ion'
|
||||
onPress={() => this.handlePress(this.goToSelectTeam)}
|
||||
|
|
|
|||
|
|
@ -1712,6 +1712,7 @@
|
|||
"mobile.loading_members": "Loading Members...",
|
||||
"mobile.loading_posts": "Loading Messages...",
|
||||
"mobile.login_options.choose_title": "Choose your login method",
|
||||
"mobile.notification.in": " in ",
|
||||
"mobile.offlineIndicator.connected": "Connected",
|
||||
"mobile.offlineIndicator.connecting": "Connecting...",
|
||||
"mobile.offlineIndicator.offline": "No internet connection",
|
||||
|
|
@ -1743,6 +1744,7 @@
|
|||
"mobile.routes.thread_dm": "Direct Message Thread",
|
||||
"mobile.routes.user_profile": "Profile",
|
||||
"mobile.routes.user_profile.send_message": "Send Message",
|
||||
"mobile.select_team.choose": "Your teams:",
|
||||
"mobile.server_ping_failed": "Cannot connect to the server. Please check your server URL and internet connection.",
|
||||
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
|
||||
"mobile.session_expired": "Session Expired: Please log in to continue receiving notifications.",
|
||||
|
|
|
|||
|
|
@ -3526,7 +3526,7 @@ makeerror@1.0.x:
|
|||
|
||||
mattermost-redux@mattermost/mattermost-redux#master:
|
||||
version "0.0.1"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/dad1a74263edb36271bf422cef977a06ac56815c"
|
||||
resolved "https://codeload.github.com/mattermost/mattermost-redux/tar.gz/6990daa5f010539075fe3657d379a6cd131b971a"
|
||||
dependencies:
|
||||
deep-equal "1.0.1"
|
||||
harmony-reflect "1.5.1"
|
||||
|
|
|
|||
Loading…
Reference in a new issue