mirror of
https://github.com/vector-im/element-ios.git
synced 2024-09-29 07:42:40 +00:00
Feature - Improve the people invite screens
#904. +"Add contact" button should be added on Room Member list #905
This commit is contained in:
parent
8efbbdfbda
commit
c03638ad4d
11 changed files with 103 additions and 53 deletions
|
@ -209,6 +209,9 @@
|
|||
F03DE2A51D0EFA6A00E8B65C /* AttachmentsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F03DE2A41D0EFA6A00E8B65C /* AttachmentsViewController.m */; };
|
||||
F046528D1E250B0A00EA4E77 /* ContactsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = F046528C1E250B0A00EA4E77 /* ContactsTableViewController.m */; };
|
||||
F046528F1E28439E00EA4E77 /* ContactsTableViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F046528E1E28439E00EA4E77 /* ContactsTableViewController.xib */; };
|
||||
F04652931E28E0E300EA4E77 /* add_participant.png in Resources */ = {isa = PBXBuildFile; fileRef = F04652901E28E0E300EA4E77 /* add_participant.png */; };
|
||||
F04652941E28E0E300EA4E77 /* add_participant@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = F04652911E28E0E300EA4E77 /* add_participant@2x.png */; };
|
||||
F04652951E28E0E300EA4E77 /* add_participant@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F04652921E28E0E300EA4E77 /* add_participant@3x.png */; };
|
||||
F047DBB51C576F2200952DA2 /* AuthenticationViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = F047DBB41C576F2200952DA2 /* AuthenticationViewController.xib */; };
|
||||
F047DBB91C576F6600952DA2 /* AuthInputsView.m in Sources */ = {isa = PBXBuildFile; fileRef = F047DBB71C576F6600952DA2 /* AuthInputsView.m */; };
|
||||
F047DBBA1C576F6600952DA2 /* AuthInputsView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F047DBB81C576F6600952DA2 /* AuthInputsView.xib */; };
|
||||
|
@ -644,6 +647,9 @@
|
|||
F046528B1E250B0A00EA4E77 /* ContactsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ContactsTableViewController.h; sourceTree = "<group>"; };
|
||||
F046528C1E250B0A00EA4E77 /* ContactsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ContactsTableViewController.m; sourceTree = "<group>"; };
|
||||
F046528E1E28439E00EA4E77 /* ContactsTableViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ContactsTableViewController.xib; sourceTree = "<group>"; };
|
||||
F04652901E28E0E300EA4E77 /* add_participant.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = add_participant.png; sourceTree = "<group>"; };
|
||||
F04652911E28E0E300EA4E77 /* add_participant@2x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "add_participant@2x.png"; sourceTree = "<group>"; };
|
||||
F04652921E28E0E300EA4E77 /* add_participant@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = "add_participant@3x.png"; sourceTree = "<group>"; };
|
||||
F047DBB41C576F2200952DA2 /* AuthenticationViewController.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AuthenticationViewController.xib; sourceTree = "<group>"; };
|
||||
F047DBB61C576F6600952DA2 /* AuthInputsView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AuthInputsView.h; sourceTree = "<group>"; };
|
||||
F047DBB71C576F6600952DA2 /* AuthInputsView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AuthInputsView.m; sourceTree = "<group>"; };
|
||||
|
@ -1144,6 +1150,9 @@
|
|||
F03BF5B41D8BF5B1002EF6A7 /* Images */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F04652901E28E0E300EA4E77 /* add_participant.png */,
|
||||
F04652911E28E0E300EA4E77 /* add_participant@2x.png */,
|
||||
F04652921E28E0E300EA4E77 /* add_participant@3x.png */,
|
||||
F04ACE001E154C540000B970 /* riot_icon.png */,
|
||||
F04ACE011E154C540000B970 /* riot_icon@2x.png */,
|
||||
F04ACE021E154C540000B970 /* riot_icon@3x.png */,
|
||||
|
@ -1793,6 +1802,7 @@
|
|||
files = (
|
||||
F0D2D9881C197DCB007B8C96 /* RoomIncomingTextMsgBubbleCell.xib in Resources */,
|
||||
F03BF6CB1D8BF5B1002EF6A7 /* settings_icon.png in Resources */,
|
||||
F04652931E28E0E300EA4E77 /* add_participant.png in Resources */,
|
||||
F03BF6791D8BF5B1002EF6A7 /* chevron@3x.png in Resources */,
|
||||
F03BF64B1D8BF5B1002EF6A7 /* admin_icon@3x.png in Resources */,
|
||||
F09EAFB61DD2109B009C7EFB /* RoomOutgoingEncryptedTextMsgWithPaginationTitleWithoutSenderNameBubbleCell.xib in Resources */,
|
||||
|
@ -1828,6 +1838,7 @@
|
|||
7165A25C1C05CD42003635D7 /* SegmentedViewController.xib in Resources */,
|
||||
F03BF65F1D8BF5B1002EF6A7 /* call_speaker_off_icon@2x.png in Resources */,
|
||||
F0A4B2F11E0073A30072D355 /* animatedLogo-1.png in Resources */,
|
||||
F04652941E28E0E300EA4E77 /* add_participant@2x.png in Resources */,
|
||||
F03BF6B41D8BF5B1002EF6A7 /* priorityLow@2x.png in Resources */,
|
||||
F08294691DB503FE00CEAB63 /* direct_icon@2x.png in Resources */,
|
||||
F04ACE041E154C540000B970 /* riot_icon@2x.png in Resources */,
|
||||
|
@ -1889,6 +1900,7 @@
|
|||
F0BE3DF21C6CE28300AC3111 /* RoomMemberDetailsViewController.xib in Resources */,
|
||||
F09EAF9C1DD2109B009C7EFB /* RoomIncomingEncryptedAttachmentWithPaginationTitleBubbleCell.xib in Resources */,
|
||||
F023A0161D9034FE00C517FB /* call_video_mute_on_icon.png in Resources */,
|
||||
F04652951E28E0E300EA4E77 /* add_participant@3x.png in Resources */,
|
||||
F03BF66A1D8BF5B1002EF6A7 /* camera_capture.png in Resources */,
|
||||
F0AF11F61D1029CF00FEE52F /* RoomIdOrAliasTableViewCell.xib in Resources */,
|
||||
F08714CC1DB9EFEE0075F633 /* directChatOff@3x.png in Resources */,
|
||||
|
|
BIN
Vector/Assets/Images/add_participant.png
Executable file
BIN
Vector/Assets/Images/add_participant.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
Vector/Assets/Images/add_participant@2x.png
Executable file
BIN
Vector/Assets/Images/add_participant@2x.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 4.3 KiB |
BIN
Vector/Assets/Images/add_participant@3x.png
Executable file
BIN
Vector/Assets/Images/add_participant@3x.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
|
@ -95,7 +95,7 @@
|
|||
"room_creation_keep_private" = "Keep private";
|
||||
"room_creation_make_private" = "Make private";
|
||||
"room_creation_wait_for_creation" = "A room is already being created. Please wait.";
|
||||
"room_creation_invite_another_user" = "Search / invite by name, email, id";
|
||||
"room_creation_invite_another_user" = "Search / invite by User ID, Name or email";
|
||||
|
||||
// Room recents
|
||||
"room_recents_directory" = "DIRECTORY";
|
||||
|
@ -111,6 +111,8 @@
|
|||
"search_messages" = "Messages";
|
||||
"search_people" = "People";
|
||||
"search_files" = "Files";
|
||||
"search_default_placeholder" = "Search...";
|
||||
"search_people_placeholder" = "Search by User ID, Name or email";
|
||||
|
||||
// Directory
|
||||
"directory_cell_title" = "Browse directory";
|
||||
|
@ -136,7 +138,7 @@
|
|||
"room_participants_invite_prompt_title" = "Invite?";
|
||||
"room_participants_invite_prompt_msg" = "Are you sure you want to invite %@ to this chat?";
|
||||
"room_participants_filter_room_members" = "Filter room members";
|
||||
"room_participants_invite_another_user" = "Search / invite by name, email, id";
|
||||
"room_participants_invite_another_user" = "Search / invite by User ID, Name or email";
|
||||
"room_participants_invite_malformed_id_title" = "Invite Error";
|
||||
"room_participants_invite_malformed_id" = "Malformed ID. Should be an email address or a Matrix ID like '@localpart:domain'";
|
||||
"room_participants_invited_section" = "INVITED";
|
||||
|
|
|
@ -84,6 +84,18 @@
|
|||
*/
|
||||
@property (nonatomic) BOOL forceMatrixIdInDisplayName;
|
||||
|
||||
/**
|
||||
The type of standard accessory view the contact cells should use
|
||||
Default is UITableViewCellAccessoryNone.
|
||||
*/
|
||||
@property (nonatomic) UITableViewCellAccessoryType contactCellAccessoryType;
|
||||
|
||||
/**
|
||||
An image used to create a custom accessy view on the right side of the contact cells.
|
||||
If set, use custom view. ignore accessoryType
|
||||
*/
|
||||
@property (nonatomic) UIImage *contactCellAccessoryImage;
|
||||
|
||||
/**
|
||||
The dictionary of the ignored local contacts, the keys are their email. Empty by default.
|
||||
*/
|
||||
|
|
|
@ -122,6 +122,8 @@
|
|||
|
||||
isMultiUseNameByDisplayName = nil;
|
||||
|
||||
_contactCellAccessoryImage = nil;
|
||||
|
||||
[super destroy];
|
||||
}
|
||||
|
||||
|
@ -466,6 +468,8 @@
|
|||
contactCell.accessoryView = nil;
|
||||
contactCell.contentView.alpha = 1;
|
||||
contactCell.userInteractionEnabled = YES;
|
||||
contactCell.accessoryType = UITableViewCellAccessoryNone;
|
||||
contactCell.accessoryView = nil;
|
||||
}
|
||||
|
||||
MXKContact *contact;
|
||||
|
@ -502,16 +506,17 @@
|
|||
{
|
||||
[contactCell render:contact];
|
||||
|
||||
// The search displays contacts to invite. Add a plus icon to the cell
|
||||
// in order to make it more understandable for the end user
|
||||
// The search displays contacts to invite.
|
||||
if (indexPath.section == filteredLocalContactsSection || indexPath.section == filteredMatrixContactsSection)
|
||||
{
|
||||
contactCell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"plus_icon"]];
|
||||
// Add the right accessory view if any
|
||||
contactCell.accessoryType = self.contactCellAccessoryType;
|
||||
contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage];
|
||||
}
|
||||
else if (indexPath.section == searchInputSection)
|
||||
{
|
||||
// This is the text entered by the user
|
||||
// Check whether the search input is a valid email or a Matrix user ID before adding the plus icon.
|
||||
// Check whether the search input is a valid email or a Matrix user ID before adding the accessory view.
|
||||
if (![MXTools isEmailAddress:currentSearchText] && ![MXTools isMatrixUserIdentifier:currentSearchText])
|
||||
{
|
||||
contactCell.contentView.alpha = 0.5;
|
||||
|
@ -519,7 +524,9 @@
|
|||
}
|
||||
else
|
||||
{
|
||||
contactCell.accessoryView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"plus_icon"]];
|
||||
// Add the right accessory view if any
|
||||
contactCell.accessoryType = self.contactCellAccessoryType;
|
||||
contactCell.accessoryView = [[UIImageView alloc] initWithImage:self.contactCellAccessoryImage];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
HomeFilesSearchViewController *filesSearchViewController;
|
||||
MXKSearchDataSource *filesSearchDataSource;
|
||||
|
||||
ContactsTableViewController *contactsViewController;
|
||||
ContactsTableViewController *peopleSearchViewController;
|
||||
MXKContact *selectedContact;
|
||||
|
||||
// Display a gradient view above the screen
|
||||
|
@ -104,9 +104,10 @@
|
|||
|
||||
// Add search People tab
|
||||
[titles addObject: NSLocalizedStringFromTable(@"search_people", @"Vector", nil)];
|
||||
contactsViewController = [ContactsTableViewController contactsTableViewController];
|
||||
contactsViewController.contactsTableViewControllerDelegate = self;
|
||||
[viewControllers addObject:contactsViewController];
|
||||
peopleSearchViewController = [ContactsTableViewController contactsTableViewController];
|
||||
peopleSearchViewController.contactsTableViewControllerDelegate = self;
|
||||
peopleSearchViewController.contactCellAccessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
[viewControllers addObject:peopleSearchViewController];
|
||||
|
||||
// add Files tab
|
||||
[titles addObject: NSLocalizedStringFromTable(@"search_files", @"Vector", nil)];
|
||||
|
@ -129,6 +130,7 @@
|
|||
[self initializeDataSources];
|
||||
|
||||
self.searchBar.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil);
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
|
@ -511,9 +513,9 @@
|
|||
{
|
||||
self.backgroundImageView.hidden = ((messagesSearchDataSource.serverCount != 0) || !messagesSearchViewController.noResultsLabel.isHidden || (self.keyboardHeight == 0));
|
||||
}
|
||||
else if (self.selectedViewController == contactsViewController)
|
||||
else if (self.selectedViewController == peopleSearchViewController)
|
||||
{
|
||||
self.backgroundImageView.hidden = (([contactsViewController.tableView numberOfRowsInSection:0] != 0) || (self.keyboardHeight == 0));
|
||||
self.backgroundImageView.hidden = (([peopleSearchViewController.tableView numberOfRowsInSection:0] != 0) || (self.keyboardHeight == 0));
|
||||
}
|
||||
else if (self.selectedViewController == filesSearchViewController)
|
||||
{
|
||||
|
@ -546,6 +548,15 @@
|
|||
|
||||
if (!self.searchBarHidden)
|
||||
{
|
||||
if (self.selectedViewController == peopleSearchViewController)
|
||||
{
|
||||
self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_people_placeholder", @"Vector", nil);
|
||||
}
|
||||
else
|
||||
{
|
||||
self.searchBar.placeholder = NSLocalizedStringFromTable(@"search_default_placeholder", @"Vector", nil);
|
||||
}
|
||||
|
||||
[self updateSearch];
|
||||
}
|
||||
}
|
||||
|
@ -1108,9 +1119,9 @@
|
|||
});
|
||||
}
|
||||
}
|
||||
else if (self.selectedViewController == contactsViewController)
|
||||
else if (self.selectedViewController == peopleSearchViewController)
|
||||
{
|
||||
[contactsViewController searchWithPattern:self.searchBar.text forceReset:NO];
|
||||
[peopleSearchViewController searchWithPattern:self.searchBar.text forceReset:NO];
|
||||
}
|
||||
else if (self.selectedViewController == filesSearchViewController)
|
||||
{
|
||||
|
@ -1138,7 +1149,7 @@
|
|||
{
|
||||
[messagesSearchDataSource searchMessages:nil force:NO];
|
||||
}
|
||||
[contactsViewController searchWithPattern:nil forceReset:NO];
|
||||
[peopleSearchViewController searchWithPattern:nil forceReset:NO];
|
||||
if (filesSearchDataSource.searchText.length)
|
||||
{
|
||||
[filesSearchDataSource searchMessages:nil force:NO];
|
||||
|
@ -1157,7 +1168,7 @@
|
|||
// As the public room search is local, it can be updated on each text change
|
||||
[self updateSearch];
|
||||
}
|
||||
else if (self.selectedViewController == contactsViewController)
|
||||
else if (self.selectedViewController == peopleSearchViewController)
|
||||
{
|
||||
// As the contact search is local, it can be updated on each text change
|
||||
[self updateSearch];
|
||||
|
|
|
@ -495,7 +495,7 @@
|
|||
|
||||
addParticipantButtonImageView.backgroundColor = [UIColor clearColor];
|
||||
addParticipantButtonImageView.contentMode = UIViewContentModeCenter;
|
||||
addParticipantButtonImageView.image = [UIImage imageNamed:@"create_room"];
|
||||
addParticipantButtonImageView.image = [UIImage imageNamed:@"add_participant"];
|
||||
|
||||
CGFloat side = 78.0f;
|
||||
NSLayoutConstraint* widthConstraint = [NSLayoutConstraint constraintWithItem:addParticipantButtonImageView
|
||||
|
@ -545,12 +545,14 @@
|
|||
|
||||
- (void)onAddParticipantButtonPressed
|
||||
{
|
||||
// Push the contacts table screen.
|
||||
// Push the contacts picker.
|
||||
contactsPickerViewController = [ContactsTableViewController contactsTableViewController];
|
||||
|
||||
// Set delegate to handle action on member (start chat, mention)
|
||||
contactsPickerViewController.contactsTableViewControllerDelegate = self;
|
||||
contactsPickerViewController.forceMatrixIdInDisplayName = YES;
|
||||
// Add a plus icon to the contact cell in the contacts picker, in order to make it more understandable for the end user.
|
||||
contactsPickerViewController.contactCellAccessoryImage = [UIImage imageNamed:@"plus_icon"];
|
||||
|
||||
// List all the participants by their matrix user id, or a room 3pid invite token to ignore them during the contacts search.
|
||||
[contactsPickerViewController.ignoredContactsByMatrixId removeAllObjects];
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
/**
|
||||
'StartChatViewController' instance is used to prepare new room creation.
|
||||
*/
|
||||
@interface StartChatViewController : ContactsTableViewController <UISearchBarDelegate>
|
||||
@interface StartChatViewController : ContactsTableViewController <UISearchBarDelegate, ContactsTableViewControllerDelegate>
|
||||
|
||||
@property (weak, nonatomic) IBOutlet UIView *searchBarHeader;
|
||||
@property (weak, nonatomic) IBOutlet UISearchBar *searchBarView;
|
||||
|
|
|
@ -67,6 +67,13 @@
|
|||
|
||||
// Prepare room participants
|
||||
participants = [NSMutableArray array];
|
||||
|
||||
// Assign itself as delegate
|
||||
self.contactsTableViewControllerDelegate = self;
|
||||
|
||||
// Add a plus icon to the contact cell when a search session is in progress,
|
||||
// in order to make it more understandable for the end user.
|
||||
self.contactCellAccessoryImage = [UIImage imageNamed:@"plus_icon"];;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
|
@ -269,14 +276,14 @@
|
|||
{
|
||||
NSInteger count = 0;
|
||||
|
||||
if (section == participantsSection)
|
||||
{
|
||||
count = participants.count + 1;
|
||||
}
|
||||
else
|
||||
if (_isAddParticipantSearchBarEditing)
|
||||
{
|
||||
count = [super tableView:self.tableView numberOfRowsInSection:section];
|
||||
}
|
||||
else if (section == participantsSection)
|
||||
{
|
||||
count = participants.count + 1;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
@ -285,7 +292,11 @@
|
|||
{
|
||||
UITableViewCell *cell;
|
||||
|
||||
if (indexPath.section == participantsSection)
|
||||
if (_isAddParticipantSearchBarEditing)
|
||||
{
|
||||
cell = [super tableView:self.tableView cellForRowAtIndexPath:indexPath];
|
||||
}
|
||||
else if (indexPath.section == participantsSection)
|
||||
{
|
||||
ContactTableViewCell* participantCell = [tableView dequeueReusableCellWithIdentifier:@"ParticipantTableViewCellId" forIndexPath:indexPath];
|
||||
|
||||
|
@ -317,10 +328,6 @@
|
|||
|
||||
cell = participantCell;
|
||||
}
|
||||
else
|
||||
{
|
||||
cell = [super tableView:self.tableView cellForRowAtIndexPath:indexPath];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
@ -345,7 +352,7 @@
|
|||
{
|
||||
CGFloat height = 0.0;
|
||||
|
||||
if (section != participantsSection)
|
||||
if (_isAddParticipantSearchBarEditing)
|
||||
{
|
||||
height = [super tableView:self.tableView heightForHeaderInSection:section];
|
||||
}
|
||||
|
@ -355,32 +362,15 @@
|
|||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSInteger row = indexPath.row;
|
||||
MXKContact *mxkContact;
|
||||
|
||||
if (indexPath.section == searchInputSection)
|
||||
if (_isAddParticipantSearchBarEditing)
|
||||
{
|
||||
mxkContact = [[MXKContact alloc] initMatrixContactWithDisplayName:currentSearchText andMatrixID:nil];
|
||||
[super tableView:tableView didSelectRowAtIndexPath:indexPath];
|
||||
}
|
||||
else if (indexPath.section == filteredLocalContactsSection)
|
||||
else
|
||||
{
|
||||
mxkContact = filteredLocalContacts[row];
|
||||
// Do nothing
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}
|
||||
else if (indexPath.section == filteredMatrixContactsSection)
|
||||
{
|
||||
mxkContact = filteredMatrixContacts[row];
|
||||
}
|
||||
|
||||
if (mxkContact)
|
||||
{
|
||||
// Update here the mutable list of participants
|
||||
[participants addObject:mxkContact];
|
||||
|
||||
// Refresh display by leaving search session
|
||||
[self searchBarCancelButtonClicked:_searchBarView];
|
||||
}
|
||||
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}
|
||||
|
||||
- (NSArray *)tableView:(UITableView *)tableView editActionsForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
|
@ -648,4 +638,18 @@
|
|||
[searchBar resignFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark - ContactsTableViewControllerDelegate
|
||||
|
||||
- (void)contactsTableViewController:(ContactsTableViewController *)contactsTableViewController didSelectContact:(MXKContact*)contact
|
||||
{
|
||||
if (contact)
|
||||
{
|
||||
// Update here the mutable list of participants
|
||||
[participants addObject:contact];
|
||||
}
|
||||
|
||||
// Refresh display by leaving search session
|
||||
[self searchBarCancelButtonClicked:_searchBarView];
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
Loading…
Reference in a new issue