Merge pull request #6679 from BlueWallet/seg

ADD: Use native segment controller.
This commit is contained in:
GLaDOS 2024-06-09 20:57:01 +00:00 committed by GitHub
commit 1a81449c40
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 325 additions and 139 deletions

View File

@ -109,6 +109,7 @@ dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.4.0'
}
apply plugin: 'com.google.gms.google-services' // Google Services plugin

View File

@ -0,0 +1,26 @@
package io.bluewallet.bluewallet;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.uimanager.ViewManager;
import com.facebook.react.bridge.ReactApplicationContext;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class CustomSegmentControlPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
return Collections.emptyList();
}
@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
CustomSegmentedControlManager.registerIfNecessary();
List<ViewManager> viewManagers = new ArrayList<>();
viewManagers.add(new CustomSegmentedControlManager());
return viewManagers;
}
}

View File

@ -0,0 +1,100 @@
package io.bluewallet.bluewallet;
import android.content.Context;
import android.widget.LinearLayout;
import androidx.annotation.NonNull;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.google.android.material.tabs.TabLayout;
import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.ReadableArray;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.events.RCTEventEmitter;
public class CustomSegmentedControlManager extends SimpleViewManager<CustomSegmentedControlManager.CustomSegmentedControlView> {
public static final String REACT_CLASS = "CustomSegmentedControl";
private static boolean isRegistered = false;
public static class CustomSegmentedControlView extends LinearLayout {
private TabLayout tabLayout;
private RCTEventEmitter eventEmitter;
private int viewId;
public CustomSegmentedControlView(Context context) {
super(context);
setOrientation(LinearLayout.HORIZONTAL);
tabLayout = new TabLayout(context);
addView(tabLayout, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
WritableMap event = Arguments.createMap();
event.putInt("selectedIndex", tab.getPosition());
if (eventEmitter != null) {
eventEmitter.receiveEvent(viewId, "topChange", event);
}
}
@Override
public void onTabUnselected(TabLayout.Tab tab) {}
@Override
public void onTabReselected(TabLayout.Tab tab) {}
});
}
public void setValues(ReadableArray values) {
tabLayout.removeAllTabs();
for (int i = 0; i < values.size(); i++) {
tabLayout.addTab(tabLayout.newTab().setText(values.getString(i)));
}
}
public void setSelectedIndex(int selectedIndex) {
if (selectedIndex >= 0 && selectedIndex < tabLayout.getTabCount()) {
TabLayout.Tab tab = tabLayout.getTabAt(selectedIndex);
if (tab != null) {
tab.select();
}
}
}
public void setEventEmitter(RCTEventEmitter eventEmitter, int viewId) {
this.eventEmitter = eventEmitter;
this.viewId = viewId;
}
}
@NonNull
@Override
public String getName() {
return REACT_CLASS;
}
@NonNull
@Override
protected CustomSegmentedControlView createViewInstance(@NonNull ThemedReactContext reactContext) {
CustomSegmentedControlView view = new CustomSegmentedControlView(reactContext);
view.setEventEmitter(reactContext.getJSModule(RCTEventEmitter.class), view.getId());
return view;
}
@ReactProp(name = "values")
public void setValues(CustomSegmentedControlView view, ReadableArray values) {
view.setValues(values);
}
@ReactProp(name = "selectedIndex")
public void setSelectedIndex(CustomSegmentedControlView view, int selectedIndex) {
view.setSelectedIndex(selectedIndex);
}
public static void registerIfNecessary() {
if (!isRegistered) {
isRegistered = true;
// Registration logic if necessary
}
}
}

View File

@ -31,7 +31,8 @@ public class MainApplication extends Application implements ReactApplication {
@SuppressWarnings("UnnecessaryLocalVariable")
List<ReactPackage> packages = new PackageList(this).getPackages();
// Packages that cannot be autolinked yet can be added manually here, for example:
// packages.add(new MyReactNativePackage());
packages.add(new CustomSegmentControlPackage());
CustomSegmentedControlManager.registerIfNecessary();
return packages;
}
@ -40,15 +41,15 @@ public class MainApplication extends Application implements ReactApplication {
return "index";
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
@Override
protected boolean isNewArchEnabled() {
return BuildConfig.IS_NEW_ARCHITECTURE_ENABLED;
}
@Override
protected Boolean isHermesEnabled() {
return BuildConfig.IS_HERMES_ENABLED;
}
};
@Override
@ -62,19 +63,19 @@ public class MainApplication extends Application implements ReactApplication {
I18nUtil sharedI18nUtilInstance = I18nUtil.getInstance();
sharedI18nUtilInstance.allowRTL(getApplicationContext(), true);
SoLoader.init(this, /* native exopackage */ false);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE);
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
// If you opted-in for the New Architecture, we load the native entry point for this app.
DefaultNewArchitectureEntryPoint.load();
}
SharedPreferences sharedPref = getApplicationContext().getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE);
// Retrieve the "donottrack" value. Default to "0" if not found.
String isDoNotTrackEnabled = sharedPref.getString("donottrack", "0");
// Retrieve the "donottrack" value. Default to "0" if not found.
String isDoNotTrackEnabled = sharedPref.getString("donottrack", "0");
// Check if do not track is not enabled and initialize Bugsnag if so
if (!isDoNotTrackEnabled.equals("1")) {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this);
}
// Check if do not track is not enabled and initialize Bugsnag if so
if (!isDoNotTrackEnabled.equals("1")) {
// Initialize Bugsnag or your error tracking here
Bugsnag.start(this);
}
}
}

View File

@ -0,0 +1,44 @@
import React from 'react';
import { requireNativeComponent, View, StyleSheet, NativeSyntheticEvent } from 'react-native';
interface SegmentedControlProps {
values: string[];
selectedIndex: number;
onChange: (index: number) => void;
}
interface SegmentedControlEvent {
selectedIndex: number;
}
interface NativeSegmentedControlProps {
values: string[];
selectedIndex: number;
onChangeEvent: (event: NativeSyntheticEvent<SegmentedControlEvent>) => void;
style?: object;
}
const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl');
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
const handleChange = (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
onChange(event.nativeEvent.selectedIndex);
};
return (
<View style={styles.container}>
<NativeSegmentedControl values={values} selectedIndex={selectedIndex} style={styles.segmentedControl} onChangeEvent={handleChange} />
</View>
);
};
const styles = StyleSheet.create({
container: {
width: '100%',
},
segmentedControl: {
height: 40,
},
});
export default SegmentedControl;

View File

@ -1,109 +0,0 @@
import React from 'react';
import { StyleSheet, Text, View, Pressable, LayoutAnimation, Platform, UIManager, ViewStyle, TextStyle } from 'react-native';
import loc from '../../loc';
import { useTheme } from '../themes';
export const TABS = {
EXTERNAL: 'receive',
INTERNAL: 'change',
} as const;
type TabKey = keyof typeof TABS;
type TABS_VALUES = (typeof TABS)[keyof typeof TABS];
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
interface AddressTypeTabsProps {
currentTab: TABS_VALUES;
setCurrentTab: (tab: TABS_VALUES) => void;
customTabText?: { [key in TabKey]?: string };
}
const AddressTypeTabs: React.FC<AddressTypeTabsProps> = ({ currentTab, setCurrentTab, customTabText }) => {
const { colors } = useTheme();
const stylesHook = StyleSheet.create({
activeTab: {
backgroundColor: colors.modal,
} as ViewStyle,
activeText: {
fontWeight: 'bold',
color: colors.foregroundColor,
} as TextStyle,
inactiveTab: {
fontWeight: 'normal',
color: colors.foregroundColor,
} as TextStyle,
backTabs: {
backgroundColor: colors.buttonDisabledBackgroundColor,
} as ViewStyle,
});
const tabs = Object.entries(TABS).map(([key, value]) => {
return {
key: key as TabKey,
value,
name: customTabText?.[key as TabKey] || loc.addresses[`type_${value}`],
};
});
const changeToTab = (tabKey: TabKey) => {
if (tabKey in TABS) {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCurrentTab(TABS[tabKey]);
}
};
const render = () => {
const tabsButtons = tabs.map(tab => {
const isActive = tab.value === currentTab;
const tabStyle = isActive ? stylesHook.activeTab : undefined;
const textStyle = isActive ? stylesHook.activeText : stylesHook.inactiveTab;
return (
<Pressable key={tab.key} onPress={() => changeToTab(tab.key)} style={[styles.tab, tabStyle]}>
<Text style={textStyle}>{tab.name}</Text>
</Pressable>
);
});
return (
<View style={styles.container}>
<View style={[stylesHook.backTabs, styles.backTabs]}>
<View style={styles.tabs}>{tabsButtons}</View>
</View>
</View>
);
};
return render();
};
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
} as ViewStyle,
backTabs: {
padding: 4,
marginVertical: 8,
borderRadius: 8,
} as ViewStyle,
tabs: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
} as ViewStyle,
tab: {
borderRadius: 6,
paddingVertical: 8,
paddingHorizontal: 16,
justifyContent: 'center',
} as ViewStyle,
});
export { AddressTypeTabs };

View File

@ -4,3 +4,4 @@
#import "AppDelegate.h"
#import <React/RCTBridgeModule.h>
#import "React/RCTViewManager.h"

View File

@ -141,8 +141,10 @@
B450109D2C0FCD9F00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109E2C0FCDA000619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B450109F2C0FCDA500619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B47462D02C1538D800100825 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B47B21EC2B2128B8001F6690 /* BlueWalletUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */; };
B49038D92B8FBAD300A8164A /* BlueWalletUITest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */; };
B4A29A2C2B55C990002A67DF /* EventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D32C5C52596CE3A008C077C /* EventEmitter.m */; };
@ -444,6 +446,8 @@
B44033FF2BCC37F800162242 /* Bundle+decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+decode.swift"; sourceTree = "<group>"; };
B440340E2BCC40A400162242 /* fiatUnits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = fiatUnits.json; path = ../../../models/fiatUnits.json; sourceTree = "<group>"; };
B450109B2C0FCD8A00619044 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControlManager.m; sourceTree = "<group>"; };
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSegmentedControlManager.h; sourceTree = "<group>"; };
B4549F352B82B10D002E3153 /* ci_post_clone.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = "<group>"; };
B461B850299599F800E431AA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = BlueWallet/AppDelegate.h; sourceTree = "<group>"; };
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
@ -715,7 +719,7 @@
83CBB9F61A601CBA00E9B192 = {
isa = PBXGroup;
children = (
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B45010A12C1504E900619044 /* Components */,
B44033C82BCC34AC00162242 /* Shared */,
B41C2E552BB3DCB8000FE097 /* PrivacyInfo.xcprivacy */,
B4549F2E2B80FEA1002E3153 /* ci_scripts */,
@ -862,6 +866,24 @@
path = Utilities;
sourceTree = "<group>";
};
B45010A12C1504E900619044 /* Components */ = {
isa = PBXGroup;
children = (
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
B45010A82C1507F000619044 /* SegmentedControl */,
);
path = Components;
sourceTree = "<group>";
};
B45010A82C1507F000619044 /* SegmentedControl */ = {
isa = PBXGroup;
children = (
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */,
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */,
);
path = SegmentedControl;
sourceTree = "<group>";
};
B4549F2E2B80FEA1002E3153 /* ci_scripts */ = {
isa = PBXGroup;
children = (
@ -1521,6 +1543,7 @@
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
13B07FC11A68108700A75B9A /* main.m in Sources */,
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
@ -1638,6 +1661,7 @@
B44033D72BCC369400162242 /* UserDefaultsExtension.swift in Sources */,
B44033F72BCC377F00162242 /* WidgetData.swift in Sources */,
B44033E12BCC36CA00162242 /* Placeholders.swift in Sources */,
B47462D02C1538D800100825 /* CustomSegmentedControlManager.m in Sources */,
B44033E72BCC36FF00162242 /* WalletData.swift in Sources */,
B44033E02BCC36C300162242 /* LatestTransaction.swift in Sources */,
B44033D92BCC369900162242 /* Colors.swift in Sources */,

View File

@ -10,6 +10,7 @@
#import <React/RCTRootView.h>
#import <Bugsnag/Bugsnag.h>
#import "BlueWallet-Swift.h"
#import "CustomSegmentedControlManager.h"
@interface AppDelegate() <UNUserNotificationCenterDelegate>
@ -21,6 +22,7 @@
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[CustomSegmentedControlManager registerIfNecessary];
[self clearFilesIfNeeded];
self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];

View File

@ -0,0 +1,15 @@
//
// SegmentedControlManager.h
// BlueWallet
//
// Created by Marcos Rodriguez on 6/8/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
#import <React/RCTViewManager.h>
@interface CustomSegmentedControlManager : RCTViewManager
+ (void)registerIfNecessary;
@end

View File

@ -0,0 +1,62 @@
#import "CustomSegmentedControlManager.h"
#import <React/RCTBridge.h>
#import <React/RCTEventDispatcher.h>
#import <React/UIView+React.h>
@interface CustomSegmentedControl : UISegmentedControl
@property (nonatomic, copy) RCTDirectEventBlock onChangeEvent;
- (void)setValues:(NSArray<NSString *> *)values;
- (void)setSelectedIndex:(NSNumber *)selectedIndex;
@end
@implementation CustomSegmentedControl
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self addTarget:self action:@selector(onChange:) forControlEvents:UIControlEventValueChanged];
}
return self;
}
- (void)setValues:(NSArray<NSString *> *)values {
[self removeAllSegments];
for (NSUInteger i = 0; i < values.count; i++) {
[self insertSegmentWithTitle:values[i] atIndex:i animated:NO];
}
}
- (void)setSelectedIndex:(NSNumber *)selectedIndex {
self.selectedSegmentIndex = selectedIndex.integerValue;
}
- (void)onChange:(UISegmentedControl *)sender {
if (self.onChangeEvent) {
self.onChangeEvent(@{@"selectedIndex": @(self.selectedSegmentIndex)});
}
}
@end
@implementation CustomSegmentedControlManager
static BOOL isRegistered = NO;
RCT_EXPORT_MODULE(CustomSegmentedControl)
- (UIView *)view {
return [CustomSegmentedControl new];
}
RCT_EXPORT_VIEW_PROPERTY(values, NSArray)
RCT_EXPORT_VIEW_PROPERTY(selectedIndex, NSNumber)
RCT_EXPORT_VIEW_PROPERTY(onChangeEvent, RCTDirectEventBlock)
+ (void)registerIfNecessary {
if (!isRegistered) {
isRegistered = YES;
// Registration logic if necessary
}
}
@end

View File

@ -3,13 +3,21 @@ import { useFocusEffect, useRoute, RouteProp } from '@react-navigation/native';
import { ActivityIndicator, FlatList, StyleSheet, View } from 'react-native';
import { WatchOnlyWallet } from '../../class';
import { AddressItem } from '../../components/addresses/AddressItem';
import { AddressTypeTabs, TABS } from '../../components/addresses/AddressTypeTabs';
import { useTheme } from '../../components/themes';
import usePrivacy from '../../hooks/usePrivacy';
import { useStorage } from '../../hooks/context/useStorage';
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { DetailViewStackParamList } from '../../navigation/DetailViewStackParamList';
import { useExtendedNavigation } from '../../hooks/useExtendedNavigation';
import SegmentedControl from '../../components/SegmentControl';
import loc from '../../loc';
export const TABS = {
EXTERNAL: 'receive',
INTERNAL: 'change',
} as const;
type TabKey = keyof typeof TABS;
interface Address {
key: string;
@ -205,10 +213,16 @@ const WalletAddresses: React.FC = () => {
centerContent={!showAddresses}
contentInsetAdjustmentBehavior="automatic"
ListHeaderComponent={
<AddressTypeTabs
currentTab={currentTab}
setCurrentTab={(tab: (typeof TABS)[keyof typeof TABS]) => dispatch({ type: SET_CURRENT_TAB, payload: tab })}
/>
<View style={styles.segmentController}>
<SegmentedControl
values={Object.values(TABS).map(tab => loc.addresses[`type_${tab}`])}
selectedIndex={Object.values(TABS).findIndex(tab => tab === currentTab)}
onChange={(index: number) => {
const tabKey = Object.keys(TABS)[index] as TabKey;
dispatch({ type: SET_CURRENT_TAB, payload: TABS[tabKey] });
}}
/>
</View>
}
/>
</View>
@ -221,4 +235,10 @@ const styles = StyleSheet.create({
root: {
flex: 1,
},
segmentController: {
margin: 40,
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
});

View File

@ -1,6 +1,5 @@
import assert from 'assert';
import { filterByAddressType, getAddress, sortByAddressIndex, totalBalance } from '../../screen/wallets/WalletAddresses';
import { TABS } from '../../components/addresses/AddressTypeTabs';
import { TABS, filterByAddressType, getAddress, sortByAddressIndex, totalBalance } from '../../screen/wallets/WalletAddresses';
jest.mock('../../blue_modules/currency', () => {
return {