Skip to content

Commit

Permalink
WIP: inline images
Browse files Browse the repository at this point in the history
  • Loading branch information
seavan committed Oct 17, 2017
1 parent 1d9d45c commit 3ceda61
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 25 deletions.
2 changes: 2 additions & 0 deletions app/components/controls/button.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ Button.propTypes = {
text: PropTypes.any.isRequired,
caps: PropTypes.bool,
disabled: PropTypes.bool,
accessible: PropTypes.bool,
accessibilityLabel: PropTypes.string,
testID: PropTypes.string,
bold: PropTypes.bool
};
149 changes: 137 additions & 12 deletions app/components/files/file-inline-image.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,155 @@
import React from 'react';
import { observer } from 'mobx-react/native';
import { observable } from 'mobx';
import { View, Image, Text } from 'react-native';
import { observable, when, reaction } from 'mobx';
import { View, Image, Text, Dimensions, LayoutAnimation, TouchableOpacity } from 'react-native';
import SafeComponent from '../shared/safe-component';
import { vars } from '../../styles/styles';
import icons from '../helpers/icons';

const OPTIMAL_WIDTH = 300;
const OPTIMAL_HEIGHT = 200;
class InlineImageCacheStore {
data = {};

async getSize(id) {
if (this.data[id]) return this.data[id];
// TODO
return new Promise(resolve =>
Image.getSize(id, (width, height) => {
const result = { width, height };
this.data[id] = result;
resolve(result);
}));
}
}

const inlineImageCacheStore = new InlineImageCacheStore();

const DISPLAY_BY_DEFAULT = false;

@observer
export default class FileInlineImage extends SafeComponent {
@observable width = 10;
@observable height = 10;
@observable width = 0;
@observable height = 0;
@observable optimalContentWidth = 0;
@observable optimalContentHeight = Dimensions.get('window').height;
@observable opened;
@observable tooBig;
@observable loadImage;
outerPadding = 8;

componentWillMount() {
Image.getSize(this.props.image, (width, height) => {
async componentWillMount() {
this.opened = DISPLAY_BY_DEFAULT;
this.tooBig = Math.random() > 0.5;
this.loadImage = DISPLAY_BY_DEFAULT && !this.tooBig;
when(() => this.loadImage, () => this.fetchSize());
}

async fetchSize() {
const { width, height } = await inlineImageCacheStore.getSize(this.props.image);
when(() => this.optimalContentWidth > 0, () => {
const { optimalContentWidth, optimalContentHeight } = this;
let w = width + 0.0, h = height + 0.0;
if (w > optimalContentWidth) {
h *= optimalContentWidth / w;
w = optimalContentWidth;
}
if (h > optimalContentHeight) {
w *= optimalContentHeight / h;
h = optimalContentHeight;
}
this.width = Math.floor(w);
this.height = Math.floor(h);
console.log(this.width, this.height);
});
}

componentDidMount() {
reaction(() => this.opened, () => LayoutAnimation.easeInEaseOut());
}

layout = (evt) => {
this.optimalContentWidth = evt.nativeEvent.layout.width - this.outerPadding * 2 - 2;
}

renderInner() {
}

get displayTooBigImageOffer() {
const outer = {
padding: this.outerPadding
};
const text0 = {
color: vars.txtDark
};
const text = {
color: vars.bg,
fontStyle: 'italic',
marginVertical: 10
};
return (
<View style={outer}>
<Text style={text0}>Images larger than 1 MB are not displayed.</Text>
<TouchableOpacity pressRetentionOffset={vars.pressRetentionOffset} onPress={() => { this.loadImage = true; }}>
<Text style={text}>Display this image anyway</Text>
</TouchableOpacity>
</View>
);
}


get displayImageOffer() {
const text = {
color: vars.bg,
fontStyle: 'italic',
textAlign: 'center',
marginVertical: 10
};
return (
<TouchableOpacity pressRetentionOffset={vars.pressRetentionOffset} onPress={() => { this.loadImage = true; }}>
<Text style={text}>Display this image</Text>
</TouchableOpacity>
);
}

renderThrow() {
const { image } = this.props;
const { url, title } = this.props.image;
const { width, height } = this;
const source = { uri: image };
const source = { uri: url };
const outer = {
padding: this.outerPadding,
borderColor: vars.lightGrayBg,
borderWidth: 1,
marginVertical: 4
};

const header = {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
marginBottom: this.opened ? 10 : 0
};

const text = {
fontWeight: 'bold',
color: vars.txtMedium
};

const inner = {
backgroundColor: vars.lightGrayBg
};
return (
<View>
<Image source={source} style={{ width, height }} />
<View style={outer} onLayout={this.layout}>
<View style={header}>
<Text style={text}>{title}</Text>
<View style={{ flexDirection: 'row' }}>
{!DISPLAY_BY_DEFAULT && icons.darkNoPadding(this.opened ? 'arrow-drop-up' : 'arrow-drop-down', () => { this.opened = !this.opened; })}
{icons.darkNoPadding('more-vert', () => this.props.onAction(this.props.image))}
</View>
</View>
<View style={inner}>
{this.opened && this.loadImage && <Image source={source} style={{ width, height }} />}
{this.opened && !this.loadImage && !this.tooBig && this.displayImageOffer }
{this.opened && !this.loadImage && this.tooBig && this.displayTooBigImageOffer }
</View>
</View>
);
}
Expand Down
39 changes: 39 additions & 0 deletions app/components/files/inline-image-action-sheet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { observer } from 'mobx-react/native';
import ActionSheet from 'react-native-actionsheet';
import SafeComponent from '../shared/safe-component';
import { tx } from '../utils/translator';

@observer
export default class InlineImageActionSheet extends SafeComponent {
RETRY_INDEX = 0;
DELETE_INDEX = 1;
CANCEL_INDEX = 2;

items = [
{ title: tx('button_retry') /* , action: () => this._message.send() */ },
{ title: tx('button_delete') /* , action: () => this._chat.removeMessage(this._message) */ },
{ title: tx('button_cancel') }
];

onPress = index => {
const { action } = this.items[index];
action && action();
};

show() {
this._actionSheet.show();
}

renderThrow() {
return (
<ActionSheet
ref={sheet => { this._actionSheet = sheet; }}
options={this.items.map(i => i.title)}
cancelButtonIndex={this.CANCEL_INDEX}
destructiveButtonIndex={this.DELETE_INDEX}
onPress={this.onPress}
/>
);
}
}
3 changes: 2 additions & 1 deletion app/components/layout/input-main-container.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default class InputMainContainer extends SafeComponent {

uploadQueue() {
const chat = chatState.currentChat;
const q = chat ? chat.uploadQueue : [];
const q = chat && chat.uploadQueue || [];
return q.map(f => (
<View style={{ margin: 12 }}>
<FileInlineProgress key={f.fileId} file={f.fileId} transparentOnFinishUpload />
Expand All @@ -80,6 +80,7 @@ export default class InputMainContainer extends SafeComponent {
</View>
<View style={s}>
<InputMain
{...this.props}
plus={this.addFiles}
sendAck={this.sendAck}
send={this.send} />
Expand Down
2 changes: 1 addition & 1 deletion app/components/layout/input-main.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export default class InputMain extends SafeComponent {
}

get canSend() {
return uiState.isAuthenticated && (this.hasText ? chatState.canSend : chatState.canSendAck);
return this.props.canSend || uiState.isAuthenticated && (this.hasText ? chatState.canSend : chatState.canSendAck);
}

renderThrow() {
Expand Down
3 changes: 2 additions & 1 deletion app/components/messaging/chat-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export default class ChatItem extends SafeComponent {
contact={i.sender}
isDeleted={i.sender ? i.sender.isDeleted : false}
files={i.files}
inlineImage={i.inlineImageUrl}
inlineImage={i.inlineImage}
receipts={i.receipts}
hideOnline
firstOfTheDay={i.firstOfTheDay}
Expand All @@ -43,6 +43,7 @@ export default class ChatItem extends SafeComponent {
onPressAvatar={onPressAvatar}
onLayout={this.props.onLayout}
onRetryCancel={this.props.onRetryCancel}
onInlineImageAction={this.props.onInlineImageAction}
noBorderBottom
collapsed={!!i.groupWithPrevious}
extraPaddingTop={8}
Expand Down
3 changes: 3 additions & 0 deletions app/components/messaging/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MessagingPlaceholder from '../messaging/messaging-placeholder';
import ChatItem from './chat-item';
import AvatarCircle from '../shared/avatar-circle';
import ChatActionSheet from './chat-action-sheet';
import InlineImageActionSheet from '../files/inline-image-action-sheet';
import contactState from '../contacts/contact-state';
import { vars } from '../../styles/styles';
import { tx } from '../utils/translator';
Expand Down Expand Up @@ -72,6 +73,7 @@ export default class Chat extends SafeComponent {
<ChatItem
key={item.id || index}
message={item}
onInlineImageAction={image => this._inlineImageActionSheet.show(image, item, this.chat)}
onRetryCancel={() => this._actionSheet.show(item, this.chat)}
onLayout={layout} />
);
Expand Down Expand Up @@ -239,6 +241,7 @@ export default class Chat extends SafeComponent {
</View>
<ProgressOverlay enabled={chatState.loading} />
<ChatActionSheet ref={sheet => (this._actionSheet = sheet)} />
<InlineImageActionSheet ref={sheet => (this._inlineImageActionSheet = sheet)} />
</View>
);
}
Expand Down
10 changes: 10 additions & 0 deletions app/components/mocks/mock-channel.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ import Chat from '../messaging/chat';
import ChannelInfo from '../messaging/channel-info';
import PopupLayout from '../layout/popup-layout';
import ChannelAddPeople from '../messaging/channel-add-people';
import InputMainContainer from '../layout/input-main-container';
import { User } from '../../lib/icebear';
import chatState from '../messaging/chat-state';
import contactState from '../contacts/contact-state';
import mockContactStore from './mock-contact-store';
import mockChatStore from './mock-chat-store';
import mockFileStore from './mock-file-store';
import routerMain from '../routes/router-main';
import routerModal from '../routes/router-modal';

Expand All @@ -21,8 +23,15 @@ export default class MockChannelCreate extends Component {
@observable showAddPeople = false;
componentWillMount() {
User.current = { activePlans: [] };
mockFileStore.install();
contactState.store = mockContactStore;
chatState.store = mockChatStore;
chatState.addAck = () => {
chatState.store.activeChat.addInlineImageMessage();
};
chatState.addMessage = message => {
chatState.store.activeChat.addRandomMessage(message);
};
routerMain.current = observable({
routeState: observable({
title: '# channel-mock',
Expand Down Expand Up @@ -85,6 +94,7 @@ export default class MockChannelCreate extends Component {
return (
<View style={{ flex: 1, flexGrow: 1 }}>
{this.body}
<InputMainContainer canSend />
<PopupLayout key="popups" />
</View>
);
Expand Down
43 changes: 35 additions & 8 deletions app/components/mocks/mock-chat-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { observable } from 'mobx';
import randomWords from 'random-words';
import capitalize from 'capitalize';
import mockContactStore from './mock-contact-store';
import mockFileStore from './mock-file-store';
import { popupCancelConfirm } from '../shared/popups';
import { TinyDb } from '../../lib/icebear';

Expand All @@ -10,6 +11,18 @@ const channelPaywallMessage =
`Peerio's basic account gets you access to 2 free channels.
If you would like to join or create another channel, please delete an existing one or check out our upgrade plans`;

const randomImages = [
'https://i.ytimg.com/vi/xC5n8f0fTeE/maxresdefault.jpg',
'http://cdn-image.travelandleisure.com/sites/default/files/styles/1600x1000/public/1487095116/forest-park-portland-oregon-FORESTBATH0217.jpg?itok=XVmUfPQB',
'http://cdn-image.travelandleisure.com/sites/default/files/styles/1600x1000/public/1487095116/yakushima-forest-japan-FORESTBATH0217.jpg?itok=mnXAvDq3',
'http://25.media.tumblr.com/865fb0f33ebdde6360be8576ad6b1978/tumblr_n08zcnLOEf1t8zamio1_250.png',
'http://globalforestlink.com/wp-content/uploads/2015/07/coniferous.jpg',
'https://upload.wikimedia.org/wikipedia/commons/thumb/8/8c/Grand_Anse_Beach_Grenada.jpg/1200px-Grand_Anse_Beach_Grenada.jpg',
'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcS5RzVGnDGecjd0b7YqxzvkkRo-6jiraf9FOMQAgChfa4WKD_6c',
'http://www.myjerseyshore.com/wp-content/themes/directorypress/thumbs//Beaches-Page-Picture.jpg',
'http://www.shoreexcursionsgroup.com/img/article/region_bermuda2.jpg'
];

class MockChannel {
@observable messages = [];
@observable id;
Expand All @@ -32,6 +45,7 @@ class MockChannel {
this.addRandomMessage();
}
this.addInlineImageMessage();
this.addFileMessage();
}
toggleFavoriteState() {
Expand Down Expand Up @@ -67,19 +81,32 @@ class MockChannel {
});
}
addRandomMessage() {
createMock(message) {
const id = randomWords({ min: 1, max: 4, join: '-' });
const text = randomWords({ min: 1, max: 4, join: ' ' });
let text = message;
if (!message && message !== false) text = randomWords({ min: 1, max: 4, join: ' ' });
const sender = this.participants[0];
this.messages.push({ id, text, sender });
const groupWithPrevious = Math.random() > 0.5;
return { id, text, sender, groupWithPrevious };
}
addRandomMessage(message) {
const m = this.createMock(message);
this.messages.push(m);
}
addInlineImageMessage() {
const id = randomWords({ min: 1, max: 4, join: '-' });
const text = randomWords({ min: 1, max: 4, join: ' ' });
const sender = this.participants[0];
const inlineImageUrl = 'https://i.ytimg.com/vi/xC5n8f0fTeE/maxresdefault.jpg';
this.messages.push({ id, text, sender, inlineImageUrl });
const m = this.createMock(false);
const title = `${randomWords({ min: 1, max: 2, join: '_' })}.png`;
const url = randomImages.random();
m.inlineImage = { url, title };
this.messages.push(m);
}
addFileMessage() {
const m = this.createMock(false);
m.files = [mockFileStore.files[0].id];
this.messages.push(m);
}
}
Expand Down
Loading

0 comments on commit 3ceda61

Please sign in to comment.