mirror of
https://github.com/metabolist/metatext.git
synced 2024-11-27 02:21:07 +00:00
Settings refactor
This commit is contained in:
parent
707eef959e
commit
94d0b2338f
15 changed files with 39 additions and 58 deletions
|
@ -94,8 +94,7 @@ private extension Account {
|
||||||
}
|
}
|
||||||
.compactMap(URL.init(string:)))
|
.compactMap(URL.init(string:)))
|
||||||
|
|
||||||
if !identityContext.appPreferences.shouldReduceMotion
|
if identityContext.appPreferences.animateAvatars == .everywhere {
|
||||||
&& identityContext.appPreferences.animateAvatars == .everywhere {
|
|
||||||
urls.insert(avatar)
|
urls.insert(avatar)
|
||||||
} else {
|
} else {
|
||||||
urls.insert(avatarStatic)
|
urls.insert(avatarStatic)
|
||||||
|
|
|
@ -14,8 +14,7 @@ extension NSMutableAttributedString {
|
||||||
let attachment = AnimatedTextAttachment()
|
let attachment = AnimatedTextAttachment()
|
||||||
let imageURL: URL?
|
let imageURL: URL?
|
||||||
|
|
||||||
if !identityContext.appPreferences.shouldReduceMotion,
|
if identityContext.appPreferences.animateCustomEmojis,
|
||||||
identityContext.appPreferences.animateCustomEmojis,
|
|
||||||
let urlString = emoji.url {
|
let urlString = emoji.url {
|
||||||
imageURL = URL(stringEscapingPath: urlString)
|
imageURL = URL(stringEscapingPath: urlString)
|
||||||
} else if let staticURLString = emoji.staticUrl {
|
} else if let staticURLString = emoji.staticUrl {
|
||||||
|
|
|
@ -192,7 +192,6 @@
|
||||||
"preferences.blocked-domains" = "Blocked Domains";
|
"preferences.blocked-domains" = "Blocked Domains";
|
||||||
"preferences.blocked-users" = "Blocked Users";
|
"preferences.blocked-users" = "Blocked Users";
|
||||||
"preferences.media" = "Media";
|
"preferences.media" = "Media";
|
||||||
"preferences.media.use-system-reduce-motion" = "Use system reduce motion setting";
|
|
||||||
"preferences.media.avatars" = "Avatars";
|
"preferences.media.avatars" = "Avatars";
|
||||||
"preferences.media.avatars.animate" = "Animate avatars";
|
"preferences.media.avatars.animate" = "Animate avatars";
|
||||||
"preferences.media.avatars.animate.everywhere" = "Everywhere";
|
"preferences.media.avatars.animate.everywhere" = "Everywhere";
|
||||||
|
|
|
@ -75,7 +75,8 @@ final class NotificationService: UNNotificationServiceExtension {
|
||||||
private extension NotificationService {
|
private extension NotificationService {
|
||||||
private static let environment = AppEnvironment.live(
|
private static let environment = AppEnvironment.live(
|
||||||
userNotificationCenter: .current(),
|
userNotificationCenter: .current(),
|
||||||
reduceMotion: { false })
|
reduceMotion: { false },
|
||||||
|
autoplayVideos: { true })
|
||||||
|
|
||||||
enum ImageError: Error {
|
enum ImageError: Error {
|
||||||
case dataMissing
|
case dataMissing
|
||||||
|
|
|
@ -14,6 +14,7 @@ public struct AppEnvironment {
|
||||||
let userDefaults: UserDefaults
|
let userDefaults: UserDefaults
|
||||||
let userNotificationClient: UserNotificationClient
|
let userNotificationClient: UserNotificationClient
|
||||||
let reduceMotion: () -> Bool
|
let reduceMotion: () -> Bool
|
||||||
|
let autoplayVideos: () -> Bool
|
||||||
let uuid: () -> UUID
|
let uuid: () -> UUID
|
||||||
let inMemoryContent: Bool
|
let inMemoryContent: Bool
|
||||||
let fixtureDatabase: IdentityDatabase?
|
let fixtureDatabase: IdentityDatabase?
|
||||||
|
@ -24,6 +25,7 @@ public struct AppEnvironment {
|
||||||
userDefaults: UserDefaults,
|
userDefaults: UserDefaults,
|
||||||
userNotificationClient: UserNotificationClient,
|
userNotificationClient: UserNotificationClient,
|
||||||
reduceMotion: @escaping () -> Bool,
|
reduceMotion: @escaping () -> Bool,
|
||||||
|
autoplayVideos: @escaping () -> Bool,
|
||||||
uuid: @escaping () -> UUID,
|
uuid: @escaping () -> UUID,
|
||||||
inMemoryContent: Bool,
|
inMemoryContent: Bool,
|
||||||
fixtureDatabase: IdentityDatabase?) {
|
fixtureDatabase: IdentityDatabase?) {
|
||||||
|
@ -33,6 +35,7 @@ public struct AppEnvironment {
|
||||||
self.userDefaults = userDefaults
|
self.userDefaults = userDefaults
|
||||||
self.userNotificationClient = userNotificationClient
|
self.userNotificationClient = userNotificationClient
|
||||||
self.reduceMotion = reduceMotion
|
self.reduceMotion = reduceMotion
|
||||||
|
self.autoplayVideos = autoplayVideos
|
||||||
self.uuid = uuid
|
self.uuid = uuid
|
||||||
self.inMemoryContent = inMemoryContent
|
self.inMemoryContent = inMemoryContent
|
||||||
self.fixtureDatabase = fixtureDatabase
|
self.fixtureDatabase = fixtureDatabase
|
||||||
|
@ -42,7 +45,9 @@ public struct AppEnvironment {
|
||||||
public extension AppEnvironment {
|
public extension AppEnvironment {
|
||||||
static let appGroup = "group.metabolist.metatext"
|
static let appGroup = "group.metabolist.metatext"
|
||||||
|
|
||||||
static func live(userNotificationCenter: UNUserNotificationCenter, reduceMotion: @escaping () -> Bool) -> Self {
|
static func live(userNotificationCenter: UNUserNotificationCenter,
|
||||||
|
reduceMotion: @escaping () -> Bool,
|
||||||
|
autoplayVideos: @escaping () -> Bool) -> Self {
|
||||||
Self(
|
Self(
|
||||||
session: URLSession.shared,
|
session: URLSession.shared,
|
||||||
webAuthSessionType: LiveWebAuthSession.self,
|
webAuthSessionType: LiveWebAuthSession.self,
|
||||||
|
@ -50,6 +55,7 @@ public extension AppEnvironment {
|
||||||
userDefaults: UserDefaults(suiteName: appGroup)!,
|
userDefaults: UserDefaults(suiteName: appGroup)!,
|
||||||
userNotificationClient: .live(userNotificationCenter),
|
userNotificationClient: .live(userNotificationCenter),
|
||||||
reduceMotion: reduceMotion,
|
reduceMotion: reduceMotion,
|
||||||
|
autoplayVideos: autoplayVideos,
|
||||||
uuid: UUID.init,
|
uuid: UUID.init,
|
||||||
inMemoryContent: false,
|
inMemoryContent: false,
|
||||||
fixtureDatabase: nil)
|
fixtureDatabase: nil)
|
||||||
|
|
|
@ -7,10 +7,12 @@ import Mastodon
|
||||||
public struct AppPreferences {
|
public struct AppPreferences {
|
||||||
private let userDefaults: UserDefaults
|
private let userDefaults: UserDefaults
|
||||||
private let systemReduceMotion: () -> Bool
|
private let systemReduceMotion: () -> Bool
|
||||||
|
private let systemAutoplayVideos: () -> Bool
|
||||||
|
|
||||||
public init(environment: AppEnvironment) {
|
public init(environment: AppEnvironment) {
|
||||||
self.userDefaults = environment.userDefaults
|
self.userDefaults = environment.userDefaults
|
||||||
self.systemReduceMotion = environment.reduceMotion
|
self.systemReduceMotion = environment.reduceMotion
|
||||||
|
self.systemAutoplayVideos = environment.autoplayVideos
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,11 +47,6 @@ public extension AppPreferences {
|
||||||
public var id: String { rawValue }
|
public var id: String { rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var useSystemReduceMotionForMedia: Bool {
|
|
||||||
get { self[.useSystemReduceMotionForMedia] ?? true }
|
|
||||||
set { self[.useSystemReduceMotionForMedia] = newValue }
|
|
||||||
}
|
|
||||||
|
|
||||||
var statusWord: StatusWord {
|
var statusWord: StatusWord {
|
||||||
get {
|
get {
|
||||||
if let rawValue = self[.statusWord] as String?,
|
if let rawValue = self[.statusWord] as String?,
|
||||||
|
@ -69,18 +66,18 @@ public extension AppPreferences {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return .everywhere
|
return systemReduceMotion() ? .never : .everywhere
|
||||||
}
|
}
|
||||||
set { self[.animateAvatars] = newValue.rawValue }
|
set { self[.animateAvatars] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var animateHeaders: Bool {
|
var animateHeaders: Bool {
|
||||||
get { self[.animateHeaders] ?? true }
|
get { self[.animateHeaders] ?? !systemReduceMotion() }
|
||||||
set { self[.animateHeaders] = newValue }
|
set { self[.animateHeaders] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var animateCustomEmojis: Bool {
|
var animateCustomEmojis: Bool {
|
||||||
get { self[.animateCustomEmojis] ?? true }
|
get { self[.animateCustomEmojis] ?? !systemReduceMotion() }
|
||||||
set { self[.animateCustomEmojis] = newValue }
|
set { self[.animateCustomEmojis] = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +88,7 @@ public extension AppPreferences {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return .always
|
return (!systemAutoplayVideos() || systemReduceMotion()) ? .never : .always
|
||||||
}
|
}
|
||||||
set { self[.autoplayGIFs] = newValue.rawValue }
|
set { self[.autoplayGIFs] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
@ -103,7 +100,7 @@ public extension AppPreferences {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
return .wifi
|
return (!systemAutoplayVideos() || systemReduceMotion()) ? .never : .wifi
|
||||||
}
|
}
|
||||||
set { self[.autoplayVideos] = newValue.rawValue }
|
set { self[.autoplayVideos] = newValue.rawValue }
|
||||||
}
|
}
|
||||||
|
@ -141,10 +138,6 @@ public extension AppPreferences {
|
||||||
set { self[.notificationSounds] = newValue.map { $0.rawValue } }
|
set { self[.notificationSounds] = newValue.map { $0.rawValue } }
|
||||||
}
|
}
|
||||||
|
|
||||||
var shouldReduceMotion: Bool {
|
|
||||||
systemReduceMotion() && useSystemReduceMotionForMedia
|
|
||||||
}
|
|
||||||
|
|
||||||
func positionBehavior(timeline: Timeline) -> PositionBehavior {
|
func positionBehavior(timeline: Timeline) -> PositionBehavior {
|
||||||
switch timeline {
|
switch timeline {
|
||||||
case .home:
|
case .home:
|
||||||
|
@ -195,7 +188,6 @@ private extension AppPreferences {
|
||||||
case statusWord
|
case statusWord
|
||||||
case requireDoubleTapToReblog
|
case requireDoubleTapToReblog
|
||||||
case requireDoubleTapToFavorite
|
case requireDoubleTapToFavorite
|
||||||
case useSystemReduceMotionForMedia
|
|
||||||
case animateAvatars
|
case animateAvatars
|
||||||
case animateHeaders
|
case animateHeaders
|
||||||
case animateCustomEmojis
|
case animateCustomEmojis
|
||||||
|
|
|
@ -24,6 +24,7 @@ public extension AppEnvironment {
|
||||||
userDefaults: userDefaults,
|
userDefaults: userDefaults,
|
||||||
userNotificationClient: userNotificationClient,
|
userNotificationClient: userNotificationClient,
|
||||||
reduceMotion: { false },
|
reduceMotion: { false },
|
||||||
|
autoplayVideos: { true },
|
||||||
uuid: uuid,
|
uuid: uuid,
|
||||||
inMemoryContent: inMemoryContent,
|
inMemoryContent: inMemoryContent,
|
||||||
fixtureDatabase: fixtureDatabase)
|
fixtureDatabase: fixtureDatabase)
|
||||||
|
|
|
@ -10,7 +10,8 @@ import ViewModels
|
||||||
class ShareExtensionNavigationViewController: UINavigationController {
|
class ShareExtensionNavigationViewController: UINavigationController {
|
||||||
private let environment = AppEnvironment.live(
|
private let environment = AppEnvironment.live(
|
||||||
userNotificationCenter: .current(),
|
userNotificationCenter: .current(),
|
||||||
reduceMotion: { UIAccessibility.isReduceMotionEnabled })
|
reduceMotion: { UIAccessibility.isReduceMotionEnabled },
|
||||||
|
autoplayVideos: { UIAccessibility.isVideoAutoplayEnabled })
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
|
@ -27,5 +27,6 @@ struct MetatextApp: App {
|
||||||
private extension MetatextApp {
|
private extension MetatextApp {
|
||||||
static let environment = AppEnvironment.live(
|
static let environment = AppEnvironment.live(
|
||||||
userNotificationCenter: .current(),
|
userNotificationCenter: .current(),
|
||||||
reduceMotion: { UIAccessibility.isReduceMotionEnabled })
|
reduceMotion: { UIAccessibility.isReduceMotionEnabled },
|
||||||
|
autoplayVideos: { UIAccessibility.isVideoAutoplayEnabled })
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ public extension AccountViewModel {
|
||||||
var id: Account.Id { accountService.account.id }
|
var id: Account.Id { accountService.account.id }
|
||||||
|
|
||||||
var headerURL: URL {
|
var headerURL: URL {
|
||||||
if !identityContext.appPreferences.shouldReduceMotion, identityContext.appPreferences.animateHeaders {
|
if identityContext.appPreferences.animateHeaders {
|
||||||
return accountService.account.header
|
return accountService.account.header
|
||||||
} else {
|
} else {
|
||||||
return accountService.account.headerStatic
|
return accountService.account.headerStatic
|
||||||
|
@ -64,9 +64,8 @@ public extension AccountViewModel {
|
||||||
var isSelf: Bool { accountService.account.id == identityContext.identity.account?.id }
|
var isSelf: Bool { accountService.account.id == identityContext.identity.account?.id }
|
||||||
|
|
||||||
func avatarURL(profile: Bool = false) -> URL {
|
func avatarURL(profile: Bool = false) -> URL {
|
||||||
if !identityContext.appPreferences.shouldReduceMotion,
|
if identityContext.appPreferences.animateAvatars == .everywhere
|
||||||
(identityContext.appPreferences.animateAvatars == .everywhere
|
|| (identityContext.appPreferences.animateAvatars == .profiles && profile) {
|
||||||
|| (identityContext.appPreferences.animateAvatars == .profiles && profile)) {
|
|
||||||
return accountService.account.avatar
|
return accountService.account.avatar
|
||||||
} else {
|
} else {
|
||||||
return accountService.account.avatarStatic
|
return accountService.account.avatarStatic
|
||||||
|
|
|
@ -82,8 +82,7 @@ public extension StatusViewModel {
|
||||||
var accountName: String { "@".appending(statusService.status.displayStatus.account.acct) }
|
var accountName: String { "@".appending(statusService.status.displayStatus.account.acct) }
|
||||||
|
|
||||||
var avatarURL: URL {
|
var avatarURL: URL {
|
||||||
if !identityContext.appPreferences.shouldReduceMotion,
|
if identityContext.appPreferences.animateAvatars == .everywhere {
|
||||||
identityContext.appPreferences.animateAvatars == .everywhere {
|
|
||||||
return statusService.status.displayStatus.account.avatar
|
return statusService.status.displayStatus.account.avatar
|
||||||
} else {
|
} else {
|
||||||
return statusService.status.displayStatus.account.avatarStatic
|
return statusService.status.displayStatus.account.avatarStatic
|
||||||
|
@ -91,8 +90,7 @@ public extension StatusViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
var rebloggerAvatarURL: URL {
|
var rebloggerAvatarURL: URL {
|
||||||
if !identityContext.appPreferences.shouldReduceMotion,
|
if identityContext.appPreferences.animateAvatars == .everywhere {
|
||||||
identityContext.appPreferences.animateAvatars == .everywhere {
|
|
||||||
return statusService.status.account.avatar
|
return statusService.status.account.avatar
|
||||||
} else {
|
} else {
|
||||||
return statusService.status.account.avatarStatic
|
return statusService.status.account.avatarStatic
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// Copyright © 2020 Metabolist. All rights reserved.
|
// Copyright © 2020 Metabolist. All rights reserved.
|
||||||
|
|
||||||
|
import Combine
|
||||||
import Mastodon
|
import Mastodon
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import ViewModels
|
import ViewModels
|
||||||
|
@ -107,40 +108,31 @@ struct PreferencesView: View {
|
||||||
Toggle("preferences.links.use-universal-links",
|
Toggle("preferences.links.use-universal-links",
|
||||||
isOn: $identityContext.appPreferences.useUniversalLinks)
|
isOn: $identityContext.appPreferences.useUniversalLinks)
|
||||||
}
|
}
|
||||||
if accessibilityReduceMotion {
|
|
||||||
Toggle("preferences.media.use-system-reduce-motion",
|
|
||||||
isOn: $identityContext.appPreferences.useSystemReduceMotionForMedia)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Group {
|
Group {
|
||||||
Picker("preferences.media.autoplay.gifs",
|
Picker("preferences.media.autoplay.gifs",
|
||||||
selection: reduceMotion ? .constant(.never) : $identityContext.appPreferences.autoplayGIFs) {
|
selection: $identityContext.appPreferences.autoplayGIFs) {
|
||||||
ForEach(AppPreferences.Autoplay.allCases) { option in
|
ForEach(AppPreferences.Autoplay.allCases) { option in
|
||||||
Text(option.localizedStringKey).tag(option)
|
Text(option.localizedStringKey).tag(option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Picker("preferences.media.autoplay.videos",
|
Picker("preferences.media.autoplay.videos",
|
||||||
selection: reduceMotion
|
selection: $identityContext.appPreferences.autoplayVideos) {
|
||||||
? .constant(.never) : $identityContext.appPreferences.autoplayVideos) {
|
|
||||||
ForEach(AppPreferences.Autoplay.allCases) { option in
|
ForEach(AppPreferences.Autoplay.allCases) { option in
|
||||||
Text(option.localizedStringKey).tag(option)
|
Text(option.localizedStringKey).tag(option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Picker("preferences.media.avatars.animate",
|
Picker("preferences.media.avatars.animate",
|
||||||
selection: reduceMotion
|
selection: $identityContext.appPreferences.animateAvatars) {
|
||||||
? .constant(.never) : $identityContext.appPreferences.animateAvatars) {
|
|
||||||
ForEach(AppPreferences.AnimateAvatars.allCases) { option in
|
ForEach(AppPreferences.AnimateAvatars.allCases) { option in
|
||||||
Text(option.localizedStringKey).tag(option)
|
Text(option.localizedStringKey).tag(option)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toggle("preferences.media.custom-emojis.animate",
|
Toggle("preferences.media.custom-emojis.animate",
|
||||||
isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateCustomEmojis)
|
isOn: $identityContext.appPreferences.animateCustomEmojis)
|
||||||
.disabled(reduceMotion)
|
|
||||||
Toggle("preferences.media.headers.animate",
|
Toggle("preferences.media.headers.animate",
|
||||||
isOn: reduceMotion ? .constant(false) : $identityContext.appPreferences.animateHeaders)
|
isOn: $identityContext.appPreferences.animateHeaders)
|
||||||
.disabled(reduceMotion)
|
|
||||||
}
|
}
|
||||||
.disabled(reduceMotion)
|
|
||||||
if viewModel.identityContext.identity.authenticated
|
if viewModel.identityContext.identity.authenticated
|
||||||
&& !viewModel.identityContext.identity.pending {
|
&& !viewModel.identityContext.identity.pending {
|
||||||
Picker("preferences.home-timeline-position-on-startup",
|
Picker("preferences.home-timeline-position-on-startup",
|
||||||
|
@ -154,12 +146,10 @@ struct PreferencesView: View {
|
||||||
}
|
}
|
||||||
.navigationTitle("preferences")
|
.navigationTitle("preferences")
|
||||||
.alertItem($viewModel.alertItem)
|
.alertItem($viewModel.alertItem)
|
||||||
|
.onReceive(NotificationCenter.default.publisher(
|
||||||
|
for: UIAccessibility.videoAutoplayStatusDidChangeNotification)) { _ in
|
||||||
|
viewModel.objectWillChange.send()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private extension PreferencesView {
|
|
||||||
var reduceMotion: Bool {
|
|
||||||
identityContext.appPreferences.shouldReduceMotion
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -195,7 +195,6 @@ private extension CompositionView {
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|
||||||
let avatarURL = $0.appPreferences.animateAvatars == .everywhere
|
let avatarURL = $0.appPreferences.animateAvatars == .everywhere
|
||||||
&& !$0.appPreferences.shouldReduceMotion
|
|
||||||
? $0.identity.account?.avatar
|
? $0.identity.account?.avatar
|
||||||
: $0.identity.account?.avatarStatic
|
: $0.identity.account?.avatarStatic
|
||||||
|
|
||||||
|
|
|
@ -73,10 +73,7 @@ private extension AutocompleteItemView {
|
||||||
switch autocompleteItemConfiguration.item {
|
switch autocompleteItemConfiguration.item {
|
||||||
case let .account(account):
|
case let .account(account):
|
||||||
let appPreferences = autocompleteItemConfiguration.identityContext.appPreferences
|
let appPreferences = autocompleteItemConfiguration.identityContext.appPreferences
|
||||||
let avatarURL = appPreferences.animateAvatars == .everywhere
|
let avatarURL = appPreferences.animateAvatars == .everywhere ? account.avatar : account.avatarStatic
|
||||||
&& !appPreferences.shouldReduceMotion
|
|
||||||
? account.avatar
|
|
||||||
: account.avatarStatic
|
|
||||||
|
|
||||||
imageView.sd_setImage(with: avatarURL)
|
imageView.sd_setImage(with: avatarURL)
|
||||||
imageView.isHidden = false
|
imageView.isHidden = false
|
||||||
|
|
|
@ -66,7 +66,6 @@ private extension SecondaryNavigationTitleView {
|
||||||
|
|
||||||
func applyViewModel() {
|
func applyViewModel() {
|
||||||
let avatarURL = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
let avatarURL = viewModel.identityContext.appPreferences.animateAvatars == .everywhere
|
||||||
&& !viewModel.identityContext.appPreferences.shouldReduceMotion
|
|
||||||
? viewModel.identityContext.identity.account?.avatar
|
? viewModel.identityContext.identity.account?.avatar
|
||||||
: viewModel.identityContext.identity.account?.avatarStatic
|
: viewModel.identityContext.identity.account?.avatarStatic
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue